From af0d1fa8920793a95fae456d1f5debdc50287eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 7 Oct 2009 11:17:50 -0300 Subject: Update Orchestra instrumentations and move part of logging to Orchestra. --- actionpack/lib/abstract_controller/logger.rb | 12 +++++----- .../lib/action_controller/caching/fragments.rb | 26 ++++++---------------- actionpack/lib/action_controller/caching/pages.rb | 10 ++------- actionpack/lib/action_controller/instrument.rb | 6 +++++ actionpack/lib/action_view/template/template.rb | 2 +- actionpack/test/controller/caching_test.rb | 9 +------- 6 files changed, 23 insertions(+), 42 deletions(-) create mode 100644 actionpack/lib/action_controller/instrument.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index f4d017b8e5..cf1a6f1240 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -10,14 +10,14 @@ module AbstractController module ClassMethods #:nodoc: # Logs a message appending the value measured. - def log_with_time(message, time, log_level=::Logger::DEBUG) + def log(message, log_level=::Logger::DEBUG) return unless logger && logger.level >= log_level - logger.add(log_level, "#{message} (%.1fms)" % time) + logger.add(log_level, message) end # Silences the logger for the duration of the block. def silence - old_logger_level, logger.level = logger.level, ::Logger::ERROR if logge + old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger yield ensure logger.level = old_logger_level if logger @@ -47,7 +47,7 @@ module AbstractController # Override process_action in the AbstractController::Base # to log details about the method. def process_action(action) - event = ActiveSupport::Orchestra.instrument(:process_action, + result = ActiveSupport::Orchestra.instrument(:process_action, :controller => self, :action => action) do super end @@ -56,13 +56,13 @@ module AbstractController log = DelayedLog.new do "\n\nProcessing #{self.class.name}\##{action_name} " \ "to #{request.formats} (for #{request_origin}) " \ - "(%.1fms) [#{request.method.to_s.upcase}]" % event.duration + "[#{request.method.to_s.upcase}]" end logger.info(log) end - event.result + result end private diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 59e24619e3..00fb55f843 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -51,40 +51,32 @@ module ActionController #:nodoc: # Writes content to the location signified by key (see expire_fragment for acceptable formats) def write_fragment(key, content, options = nil) return content unless cache_configured? - key = fragment_cache_key(key) - event = ActiveSupport::Orchestra.instrument(:write_fragment, :key => key) do + + ActiveSupport::Orchestra.instrument(:write_fragment, :key => key) do cache_store.write(key, content, options) end - - self.class.log_with_time("Cached fragment miss: #{key}", event.duration) content end # Reads a cached fragment from the location signified by key (see expire_fragment for acceptable formats) def read_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) - event = ActiveSupport::Orchestra.instrument(:read_fragment, :key => key) do + + ActiveSupport::Orchestra.instrument(:read_fragment, :key => key) do cache_store.read(key, options) end - - self.class.log_with_time("Cached fragment hit: #{key}", event.duration) - event.result end # Check if a cached fragment from the location signified by key exists (see expire_fragment for acceptable formats) def fragment_exist?(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) - event = ActiveSupport::Orchestra.instrument(:fragment_exist?, :key => key) do + + ActiveSupport::Orchestra.instrument(:fragment_exist?, :key => key) do cache_store.exist?(key, options) end - - self.class.log_with_time("Cached fragment exists?: #{key}", event.duration) - event.result end # Removes fragments from the cache. @@ -106,11 +98,10 @@ module ActionController #:nodoc: # method (or delete_matched, for Regexp keys.) def expire_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) unless key.is_a?(Regexp) message = nil - event = ActiveSupport::Orchestra.instrument(:expire_fragment, :key => key) do + ActiveSupport::Orchestra.instrument(:expire_fragment, :key => key) do if key.is_a?(Regexp) message = "Expired fragments matching: #{key.source}" cache_store.delete_matched(key, options) @@ -119,9 +110,6 @@ module ActionController #:nodoc: cache_store.delete(key, options) end end - - self.class.log_with_time(message, event.duration) - event.result end end end diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 4fb154470f..72551d1fd6 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -64,12 +64,9 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - event = ActiveSupport::Orchestra.instrument(:expire_page, :path => path) do + ActiveSupport::Orchestra.instrument(:expire_page, :path => path) do File.delete(path) if File.exist?(path) end - - log_with_time("Expired page: #{path}", event.duration) - event.result end # Manually cache the +content+ in the key determined by +path+. Example: @@ -78,13 +75,10 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - event = ActiveSupport::Orchestra.instrument(:cache_page, :path => path) do + ActiveSupport::Orchestra.instrument(:cache_page, :path => path) do FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } end - - log_with_time("Cached page: #{path}", event.duration) - event.result end # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that diff --git a/actionpack/lib/action_controller/instrument.rb b/actionpack/lib/action_controller/instrument.rb new file mode 100644 index 0000000000..00888c425f --- /dev/null +++ b/actionpack/lib/action_controller/instrument.rb @@ -0,0 +1,6 @@ +require 'active_support/orchestra' + +ActiveSupport::Orchestra.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |event| + human_name = event.name.to_s.humanize + ActionController::Base.log("#{human_name} (%.1fms)" % event.duration) +end diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb index 0f64c23649..84105bd05d 100644 --- a/actionpack/lib/action_view/template/template.rb +++ b/actionpack/lib/action_view/template/template.rb @@ -30,7 +30,7 @@ module ActionView ActiveSupport::Orchestra.instrument(:render_template, :identifier => identifier) do method_name = compile(locals, view) view.send(method_name, locals, &block) - end.result + end rescue Exception => e if e.is_a?(TemplateError) e.sub_template_of(self) diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 495b431307..99d7192a9e 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -628,20 +628,13 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_for_logging fragment_computed = false - - listener = [] - ActiveSupport::Orchestra.register listener + ActiveSupport::Orchestra.queue.expects(:publish).times(4) buffer = 'generated till now -> ' @controller.fragment_for(buffer, 'expensive') { fragment_computed = true } - assert_equal 1, listener.count { |e| e.name == :fragment_exist? } - assert_equal 1, listener.count { |e| e.name == :write_fragment } - assert fragment_computed assert_equal 'generated till now -> ', buffer - ensure - ActiveSupport::Orchestra.unregister listener end end -- cgit v1.2.3 From 8f59d7a8d8e736d7f4b6730020c197d008fb0779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 9 Oct 2009 08:22:42 -0300 Subject: Instrument cache store events only if required. --- actionpack/test/controller/caching_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 99d7192a9e..3408233b66 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -628,7 +628,7 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_for_logging fragment_computed = false - ActiveSupport::Orchestra.queue.expects(:publish).times(4) + ActiveSupport::Orchestra.queue.expects(:publish).times(2) buffer = 'generated till now -> ' @controller.fragment_for(buffer, 'expensive') { fragment_computed = true } -- cgit v1.2.3 From a15e02d44ac2afb27a7e8e652c98a796d271b645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 9 Oct 2009 09:52:25 -0300 Subject: Unify benchmark APIs. --- actionpack/lib/abstract_controller/logger.rb | 18 +---- actionpack/lib/action_controller/instrument.rb | 6 +- .../lib/action_controller/metal/benchmarking.rb | 23 +----- actionpack/lib/action_view/helpers.rb | 6 +- .../lib/action_view/helpers/benchmark_helper.rb | 54 -------------- actionpack/test/template/benchmark_helper_test.rb | 86 ---------------------- 6 files changed, 12 insertions(+), 181 deletions(-) delete mode 100644 actionpack/lib/action_view/helpers/benchmark_helper.rb delete mode 100644 actionpack/test/template/benchmark_helper_test.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index cf1a6f1240..ebdeab3dfb 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/logger' +require 'active_support/benchmarkable' module AbstractController module Logger @@ -6,22 +7,7 @@ module AbstractController included do cattr_accessor :logger - end - - module ClassMethods #:nodoc: - # Logs a message appending the value measured. - def log(message, log_level=::Logger::DEBUG) - return unless logger && logger.level >= log_level - logger.add(log_level, message) - end - - # Silences the logger for the duration of the block. - def silence - old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger - yield - ensure - logger.level = old_logger_level if logger - end + extend ActiveSupport::Benchmarkable end # A class that allows you to defer expensive processing diff --git a/actionpack/lib/action_controller/instrument.rb b/actionpack/lib/action_controller/instrument.rb index 00888c425f..2c87ed7391 100644 --- a/actionpack/lib/action_controller/instrument.rb +++ b/actionpack/lib/action_controller/instrument.rb @@ -1,6 +1,8 @@ require 'active_support/orchestra' ActiveSupport::Orchestra.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |event| - human_name = event.name.to_s.humanize - ActionController::Base.log("#{human_name} (%.1fms)" % event.duration) + if logger = ActionController::Base.logger + human_name = event.name.to_s.humanize + logger.info("#{human_name} (%.1fms)" % event.duration) + end end diff --git a/actionpack/lib/action_controller/metal/benchmarking.rb b/actionpack/lib/action_controller/metal/benchmarking.rb index d4cb1e122d..e58df69172 100644 --- a/actionpack/lib/action_controller/metal/benchmarking.rb +++ b/actionpack/lib/action_controller/metal/benchmarking.rb @@ -1,4 +1,4 @@ -require 'benchmark' +require 'active_support/core_ext/benchmark' module ActionController #:nodoc: # The benchmarking module times the performance of actions and reports to the logger. If the Active Record @@ -6,25 +6,6 @@ module ActionController #:nodoc: module Benchmarking #:nodoc: extend ActiveSupport::Concern - module ClassMethods - # Log and benchmark the workings of a single block and silence whatever logging that may have happened inside it - # (unless use_silence is set to false). - # - # The benchmark is only recorded if the current level of the logger matches the log_level, which makes it - # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark - # will only be conducted if the log level is low enough. - def benchmark(title, log_level = Logger::DEBUG, use_silence = true) - if logger && logger.level == log_level - result = nil - ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } - logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)") - result - else - yield - end - end - end - protected def render(*args, &block) if logger @@ -45,7 +26,7 @@ module ActionController #:nodoc: else super end - end + end private def process_action(*args) diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index d63e8602f1..3d088678fc 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -1,10 +1,11 @@ +require 'active_support/benchmarkable' + module ActionView #:nodoc: module Helpers #:nodoc: autoload :ActiveModelHelper, 'action_view/helpers/active_model_helper' autoload :AjaxHelper, 'action_view/helpers/ajax_helper' autoload :AssetTagHelper, 'action_view/helpers/asset_tag_helper' autoload :AtomFeedHelper, 'action_view/helpers/atom_feed_helper' - autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper' autoload :CacheHelper, 'action_view/helpers/cache_helper' autoload :CaptureHelper, 'action_view/helpers/capture_helper' autoload :DateHelper, 'action_view/helpers/date_helper' @@ -33,10 +34,11 @@ module ActionView #:nodoc: include SanitizeHelper::ClassMethods end + include ActiveSupport::Benchmarkable + include ActiveModelHelper include AssetTagHelper include AtomFeedHelper - include BenchmarkHelper include CacheHelper include CaptureHelper include DateHelper diff --git a/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb deleted file mode 100644 index c13a069a35..0000000000 --- a/actionpack/lib/action_view/helpers/benchmark_helper.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'active_support/core_ext/benchmark' - -module ActionView - module Helpers - # This helper offers a method to measure the execution time of a block - # in a template. - module BenchmarkHelper - # Allows you to measure the execution time of a block - # in a template and records the result to the log. Wrap this block around - # expensive operations or possible bottlenecks to get a time reading - # for the operation. For example, let's say you thought your file - # processing method was taking too long; you could wrap it in a benchmark block. - # - # <% benchmark "Process data files" do %> - # <%= expensive_files_operation %> - # <% end %> - # - # That would add something like "Process data files (345.2ms)" to the log, - # which you can then use to compare timings when optimizing your code. - # - # You may give an optional logger level as the :level option. - # (:debug, :info, :warn, :error); the default value is :info. - # - # <% benchmark "Low-level files", :level => :debug do %> - # <%= lowlevel_files_operation %> - # <% end %> - # - # Finally, you can pass true as the third argument to silence all log activity - # inside the block. This is great for boiling down a noisy block to just a single statement: - # - # <% benchmark "Process data files", :level => :info, :silence => true do %> - # <%= expensive_and_chatty_files_operation %> - # <% end %> - def benchmark(message = "Benchmarking", options = {}) - if controller.logger - if options.is_a?(Symbol) - ActiveSupport::Deprecation.warn("use benchmark('#{message}', :level => :#{options}) instead", caller) - options = { :level => options, :silence => false } - else - options.assert_valid_keys(:level, :silence) - options[:level] ||= :info - end - - result = nil - ms = Benchmark.ms { result = options[:silence] ? controller.logger.silence { yield } : yield } - controller.logger.send(options[:level], '%s (%.1fms)' % [ message, ms ]) - result - else - yield - end - end - end - end -end diff --git a/actionpack/test/template/benchmark_helper_test.rb b/actionpack/test/template/benchmark_helper_test.rb deleted file mode 100644 index ac31fc6503..0000000000 --- a/actionpack/test/template/benchmark_helper_test.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'abstract_unit' -require 'action_view/helpers/benchmark_helper' - -class BenchmarkHelperTest < ActionView::TestCase - tests ActionView::Helpers::BenchmarkHelper - - def setup - super - controller.logger = ActiveSupport::BufferedLogger.new(StringIO.new) - controller.logger.auto_flushing = false - end - - def teardown - controller.logger.send(:clear_buffer) - end - - def test_without_block - assert_raise(LocalJumpError) { benchmark } - assert buffer.empty? - end - - def test_defaults - i_was_run = false - benchmark { i_was_run = true } - assert i_was_run - assert_last_logged - end - - def test_with_message - i_was_run = false - benchmark('test_run') { i_was_run = true } - assert i_was_run - assert_last_logged 'test_run' - end - - def test_with_message_and_deprecated_level - i_was_run = false - - assert_deprecated do - benchmark('debug_run', :debug) { i_was_run = true } - end - - assert i_was_run - assert_last_logged 'debug_run' - end - - def test_within_level - controller.logger.level = ActiveSupport::BufferedLogger::DEBUG - benchmark('included_debug_run', :level => :debug) { } - assert_last_logged 'included_debug_run' - end - - def test_outside_level - controller.logger.level = ActiveSupport::BufferedLogger::ERROR - benchmark('skipped_debug_run', :level => :debug) { } - assert_no_match(/skipped_debug_run/, buffer.last) - ensure - controller.logger.level = ActiveSupport::BufferedLogger::DEBUG - end - - def test_without_silencing - benchmark('debug_run', :silence => false) do - controller.logger.info "not silenced!" - end - - assert_equal 2, buffer.size - end - - def test_with_silencing - benchmark('debug_run', :silence => true) do - controller.logger.info "silenced!" - end - - assert_equal 1, buffer.size - end - - - private - def buffer - controller.logger.send(:buffer) - end - - def assert_last_logged(message = 'Benchmarking') - assert_match(/^#{message} \(.*\)$/, buffer.last) - end -end -- cgit v1.2.3 From 2d7abe245e7a2b1717e48ef550e4083318fd7ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Oct 2009 18:51:51 -0300 Subject: Renamed Orchestra to Notifications once again [#3321 state:resolved] --- actionpack/lib/abstract_controller/logger.rb | 2 +- actionpack/lib/action_controller/caching/fragments.rb | 8 ++++---- actionpack/lib/action_controller/caching/pages.rb | 4 ++-- actionpack/lib/action_controller/instrument.rb | 8 -------- actionpack/lib/action_controller/notifications.rb | 8 ++++++++ actionpack/lib/action_view/template/template.rb | 2 +- actionpack/test/controller/caching_test.rb | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 actionpack/lib/action_controller/instrument.rb create mode 100644 actionpack/lib/action_controller/notifications.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index ebdeab3dfb..27ba5be45f 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -33,7 +33,7 @@ module AbstractController # Override process_action in the AbstractController::Base # to log details about the method. def process_action(action) - result = ActiveSupport::Orchestra.instrument(:process_action, + result = ActiveSupport::Notifications.instrument(:process_action, :controller => self, :action => action) do super end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 00fb55f843..8c1167d526 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -53,7 +53,7 @@ module ActionController #:nodoc: return content unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Orchestra.instrument(:write_fragment, :key => key) do + ActiveSupport::Notifications.instrument(:write_fragment, :key => key) do cache_store.write(key, content, options) end content @@ -64,7 +64,7 @@ module ActionController #:nodoc: return unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Orchestra.instrument(:read_fragment, :key => key) do + ActiveSupport::Notifications.instrument(:read_fragment, :key => key) do cache_store.read(key, options) end end @@ -74,7 +74,7 @@ module ActionController #:nodoc: return unless cache_configured? key = fragment_cache_key(key) - ActiveSupport::Orchestra.instrument(:fragment_exist?, :key => key) do + ActiveSupport::Notifications.instrument(:fragment_exist?, :key => key) do cache_store.exist?(key, options) end end @@ -101,7 +101,7 @@ module ActionController #:nodoc: key = fragment_cache_key(key) unless key.is_a?(Regexp) message = nil - ActiveSupport::Orchestra.instrument(:expire_fragment, :key => key) do + ActiveSupport::Notifications.instrument(:expire_fragment, :key => key) do if key.is_a?(Regexp) message = "Expired fragments matching: #{key.source}" cache_store.delete_matched(key, options) diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 72551d1fd6..d46f528c7e 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -64,7 +64,7 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - ActiveSupport::Orchestra.instrument(:expire_page, :path => path) do + ActiveSupport::Notifications.instrument(:expire_page, :path => path) do File.delete(path) if File.exist?(path) end end @@ -75,7 +75,7 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - ActiveSupport::Orchestra.instrument(:cache_page, :path => path) do + ActiveSupport::Notifications.instrument(:cache_page, :path => path) do FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } end diff --git a/actionpack/lib/action_controller/instrument.rb b/actionpack/lib/action_controller/instrument.rb deleted file mode 100644 index 2c87ed7391..0000000000 --- a/actionpack/lib/action_controller/instrument.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'active_support/orchestra' - -ActiveSupport::Orchestra.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |event| - if logger = ActionController::Base.logger - human_name = event.name.to_s.humanize - logger.info("#{human_name} (%.1fms)" % event.duration) - end -end diff --git a/actionpack/lib/action_controller/notifications.rb b/actionpack/lib/action_controller/notifications.rb new file mode 100644 index 0000000000..4ec88193d5 --- /dev/null +++ b/actionpack/lib/action_controller/notifications.rb @@ -0,0 +1,8 @@ +require 'active_support/notifications' + +ActiveSupport::Notifications.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |event| + if logger = ActionController::Base.logger + human_name = event.name.to_s.humanize + logger.info("#{human_name} (%.1fms)" % event.duration) + end +end diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb index 84105bd05d..d1970ca3c7 100644 --- a/actionpack/lib/action_view/template/template.rb +++ b/actionpack/lib/action_view/template/template.rb @@ -27,7 +27,7 @@ module ActionView end def render(view, locals, &block) - ActiveSupport::Orchestra.instrument(:render_template, :identifier => identifier) do + ActiveSupport::Notifications.instrument(:render_template, :identifier => identifier) do method_name = compile(locals, view) view.send(method_name, locals, &block) end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 3408233b66..df2dee8228 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -628,7 +628,7 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_for_logging fragment_computed = false - ActiveSupport::Orchestra.queue.expects(:publish).times(2) + ActiveSupport::Notifications.queue.expects(:publish).times(2) buffer = 'generated till now -> ' @controller.fragment_for(buffer, 'expensive') { fragment_computed = true } -- cgit v1.2.3 From a565c19c5ba465c74a465b55be97a4abfb760e20 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 16 Oct 2009 11:26:19 -0700 Subject: Fix a bug where templates with locales were not being sorted correctly --- actionpack/lib/action_view/template/resolver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index f5591ead09..7336114e1b 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -121,7 +121,7 @@ module ActionView # # :api: plugin def path_to_details(path) # [:erb, :format => :html, :locale => :en, :partial => true/false] - if m = path.match(%r'(?:^|/)(_)?[\w-]+(\.[\w-]+)*\.(\w+)$') + if m = path.match(%r'(?:^|/)(_)?[\w-]+((?:\.[\w-]+)*)\.(\w+)$') partial = m[1] == '_' details = (m[2]||"").split('.').reject { |e| e.empty? } handler = Template.handler_class_for_extension(m[3]) -- cgit v1.2.3 From 6094e6516951444f8c3d6bb31f171af9fe96a02f Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 16 Oct 2009 13:56:59 -0500 Subject: We won't be publishing tars and zips anymore --- actionpack/Rakefile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'actionpack') diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 157e3efd3c..99bdcc95fa 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -56,7 +56,7 @@ Rake::RDocTask.new { |rdoc| rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '--charset' << 'utf-8' rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - if ENV['DOC_FILES'] + if ENV['DOC_FILES'] rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) else rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') @@ -70,8 +70,6 @@ spec = eval(File.read('actionpack.gemspec')) Rake::GemPackageTask.new(spec) do |p| p.gem_spec = spec - p.need_tar = true - p.need_zip = true end task :lines do @@ -88,10 +86,10 @@ task :lines do codelines += 1 end puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - + total_lines += lines total_codelines += codelines - + lines, codelines = 0, 0 end @@ -112,14 +110,14 @@ task :update_js => [ :update_scriptaculous ] # Publishing ------------------------------------------------------ desc "Publish the API documentation" -task :pgem => [:package] do +task :pgem => [:package] do require 'rake/contrib/sshpublisher' Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'` end desc "Publish the API documentation" -task :pdoc => [:rdoc] do +task :pdoc => [:rdoc] do require 'rake/contrib/sshpublisher' Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload end -- cgit v1.2.3 From 2110a524a4913815d036786aa01319fd67db0ee2 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Fri, 16 Oct 2009 12:49:39 -0700 Subject: Deprecate RAILS_ROOT in favor of Rails.root (which proxies to the application's object root) --- actionpack/lib/action_controller/metal/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 7c52779064..117ea3481f 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -54,7 +54,7 @@ module ActionController included do # Set the default directory for helpers extlib_inheritable_accessor(:helpers_dir) do - defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers" + defined?(Rails) ? "#{Rails.root}/app/helpers" : "app/helpers" end end -- cgit v1.2.3 From c1261b5484c10930be9f737abfc164f9ba072629 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Fri, 16 Oct 2009 19:04:28 -0500 Subject: Use Rails.application where we want a valid rack app --- actionpack/lib/action_controller/dispatch/dispatcher.rb | 2 +- actionpack/lib/action_dispatch/testing/integration.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb index 008fb54715..e04da42637 100644 --- a/actionpack/lib/action_controller/dispatch/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb @@ -50,7 +50,7 @@ module ActionController def new # DEPRECATE Rails application fallback - Rails.application.new + Rails.application end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 58ebe94a5b..40d6f97b2a 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -490,7 +490,7 @@ module ActionDispatch def self.app # DEPRECATE Rails application fallback # This should be set by the initializer - @@app || (defined?(Rails.application) && Rails.application.new) || nil + @@app || (defined?(Rails.application) && Rails.application) || nil end def self.app=(app) -- cgit v1.2.3 From 7e9e370e031c20e36a52c04a20dbbd7b4fe4672f Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 16 Oct 2009 17:28:29 -0700 Subject: Make encodings work with Erubis and 1.9 again --- actionpack/lib/action_view/template/handlers/erb.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 8993dd2b35..88aeb4b053 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -42,9 +42,11 @@ module ActionView self.erubis_implementation = Erubis def compile(template) - magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)\s*-?%>)/ - erb = "#{magic}<% __in_erb_template=true %>#{template.source}" - self.class.erubis_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + source = template.source.gsub(/\A(<%(#.*coding[:=]\s*(\S+)\s*)-?%>)\s*\n?/, '') + erb = "<% __in_erb_template=true %>#{source}" + result = self.class.erubis_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + result = "#{$2}\n#{result}" if $2 + result end end end -- cgit v1.2.3 From 028911ad00e7f7733064a80393a1548401bba7af Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Fri, 16 Oct 2009 17:39:12 -0700 Subject: Use rails/rack --- actionpack/Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/Gemfile b/actionpack/Gemfile index 506458365f..c236169b20 100644 --- a/actionpack/Gemfile +++ b/actionpack/Gemfile @@ -2,7 +2,7 @@ rails_root = Pathname.new(File.dirname(__FILE__)).join("..") Gem.sources.each { |uri| source uri } -gem "rack", "1.0.1", :git => "git://github.com/rack/rack.git", :branch => "rack-1.0" +gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git", :branch => "rack-1.0" gem "rack-test", "~> 0.5.0" gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport") gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel") -- cgit v1.2.3 From 2e37effd7203cad84459661e11db2be44586cb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 17 Oct 2009 12:54:03 -0300 Subject: Unify class_inheritable_accessor and extlib_inheritable_accessor and allow responder to be set in the class level. --- actionpack/lib/action_controller/metal/mime_responds.rb | 9 +++------ actionpack/test/controller/mime_responds_test.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 3026067868..468c5f4fae 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -3,7 +3,8 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - class_inheritable_reader :mimes_for_respond_to + extlib_inheritable_accessor :responder, :mimes_for_respond_to, :instance_writer => false + self.responder = ActionController::Responder clear_respond_to end @@ -46,7 +47,7 @@ module ActionController #:nodoc: # Clear all mimes in respond_to. # def clear_respond_to - write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new) + self.mimes_for_respond_to = ActiveSupport::OrderedHash.new end end @@ -221,10 +222,6 @@ module ActionController #:nodoc: end end - def responder - ActionController::Responder - end - protected # Collect mimes declared in the class method respond_to valid for the diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index a79648396c..b070f925d4 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -760,6 +760,14 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal "Resource name is david", @response.body end + def test_using_resource_with_responder + RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } + get :using_resource + assert_equal "Resource name is david", @response.body + ensure + RespondWithController.responder = ActionController::Responder + end + def test_not_acceptable @request.accept = "application/xml" get :using_defaults -- cgit v1.2.3 From cb873026898badc5c3dc61a95a08051cf218251f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 17 Oct 2009 13:03:28 -0300 Subject: Bring agnosticism to error_messages_for. --- .../lib/action_view/helpers/active_model_helper.rb | 16 +- actionpack/lib/action_view/locale/en.yml | 4 +- .../test/template/active_model_helper_i18n_test.rb | 42 +++ .../test/template/active_model_helper_test.rb | 314 +++++++++++++++++++++ .../template/active_record_helper_i18n_test.rb | 51 ---- .../test/template/active_record_helper_test.rb | 314 --------------------- 6 files changed, 368 insertions(+), 373 deletions(-) create mode 100644 actionpack/test/template/active_model_helper_i18n_test.rb create mode 100644 actionpack/test/template/active_model_helper_test.rb delete mode 100644 actionpack/test/template/active_record_helper_i18n_test.rb delete mode 100644 actionpack/test/template/active_record_helper_test.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 7cc1e48572..3c398fe4da 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -202,8 +202,8 @@ module ActionView end objects.compact! - count = objects.inject(0) {|sum, object| sum + object.errors.count } + unless count.zero? html = {} [:id, :class].each do |key| @@ -216,16 +216,20 @@ module ActionView end options[:object_name] ||= params.first - I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale| + I18n.with_options :locale => options[:locale], :scope => [:activemodel, :errors, :template] do |locale| header_message = if options.include?(:header_message) options[:header_message] else - object_name = options[:object_name].to_s.gsub('_', ' ') - object_name = I18n.t(options[:object_name].to_s, :default => object_name, :scope => [:activerecord, :models], :count => 1) - locale.t :header, :count => count, :model => object_name + locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ') end + message = options.include?(:message) ? options[:message] : locale.t(:body) - error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, ERB::Util.html_escape(msg)) } }.join + + error_messages = objects.sum do |object| + object.errors.full_messages.map do |msg| + content_tag(:li, ERB::Util.html_escape(msg)) + end + end.join contents = '' contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank? diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 84d94fd700..5e2a92b89a 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -102,7 +102,7 @@ minute: "Minute" second: "Seconds" - activerecord: + activemodel: errors: template: header: @@ -114,4 +114,4 @@ support: select: # default value for :prompt => true in FormOptionsHelper - prompt: "Please select" \ No newline at end of file + prompt: "Please select" diff --git a/actionpack/test/template/active_model_helper_i18n_test.rb b/actionpack/test/template/active_model_helper_i18n_test.rb new file mode 100644 index 0000000000..2465444fc5 --- /dev/null +++ b/actionpack/test/template/active_model_helper_i18n_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' + +class ActiveModelHelperI18nTest < Test::Unit::TestCase + include ActionView::Context + include ActionView::Helpers::ActiveModelHelper + + attr_reader :request + + def setup + @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) + @object.stubs :to_model => @object + @object.stubs :class => stub(:model_name => stub(:human => "")) + + @object_name = 'book_seller' + @object_name_without_underscore = 'book seller' + + stubs(:content_tag).returns 'content_tag' + + I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved" + I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).returns 'There were problems with the following fields:' + end + + def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message + I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').never + error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en') + end + + def test_error_messages_for_given_no_header_option_it_translates_header_message + I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:activemodel, :errors, :template], :count => 1, :model => '').returns 'header message' + error_messages_for(:object => @object, :locale => 'en') + end + + def test_error_messages_for_given_a_message_option_it_does_not_translate_message + I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).never + error_messages_for(:object => @object, :message => 'message', :locale => 'en') + end + + def test_error_messages_for_given_no_message_option_it_translates_message + I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activemodel, :errors, :template]).returns 'There were problems with the following fields:' + error_messages_for(:object => @object, :locale => 'en') + end +end diff --git a/actionpack/test/template/active_model_helper_test.rb b/actionpack/test/template/active_model_helper_test.rb new file mode 100644 index 0000000000..3e01ae78c3 --- /dev/null +++ b/actionpack/test/template/active_model_helper_test.rb @@ -0,0 +1,314 @@ +require 'abstract_unit' + +class ActiveModelHelperTest < ActionView::TestCase + tests ActionView::Helpers::ActiveModelHelper + + silence_warnings do + class Post < Struct.new(:title, :author_name, :body, :secret, :written_on) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + + class User < Struct.new(:email) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + + class Column < Struct.new(:type, :name, :human_name) + extend ActiveModel::Naming + include ActiveModel::Conversion + end + end + + class DirtyPost + class Errors + def empty? + false + end + + def count + 1 + end + + def full_messages + ["Author name can't be empty"] + end + + def [](field) + ["can't be empty"] + end + end + + def errors + Errors.new + end + end + + def setup_post + @post = Post.new + def @post.errors + Class.new { + def [](field) + case field.to_s + when "author_name" + ["can't be empty"] + when "body" + ['foo'] + else + [] + end + end + def empty?() false end + def count() 1 end + def full_messages() [ "Author name can't be empty" ] end + }.new + end + + def @post.new_record?() true end + def @post.to_param() nil end + + def @post.column_for_attribute(attr_name) + Post.content_columns.select { |column| column.name == attr_name }.first + end + + silence_warnings do + def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end + end + + @post.title = "Hello World" + @post.author_name = "" + @post.body = "Back to the hill and over it again!" + @post.secret = 1 + @post.written_on = Date.new(2004, 6, 15) + end + + def setup_user + @user = User.new + def @user.errors + Class.new { + def [](field) field == "email" ? ['nonempty'] : [] end + def empty?() false end + def count() 1 end + def full_messages() [ "User email can't be empty" ] end + }.new + end + + def @user.new_record?() true end + def @user.to_param() nil end + + def @user.column_for_attribute(attr_name) + User.content_columns.select { |column| column.name == attr_name }.first + end + + silence_warnings do + def User.content_columns() [ Column.new(:string, "email", "Email") ] end + end + + @user.email = "" + end + + def protect_against_forgery? + @protect_against_forgery ? true : false + end + attr_accessor :request_forgery_protection_token, :form_authenticity_token + + def setup + super + setup_post + setup_user + + @response = ActionController::TestResponse.new + + @controller = Object.new + def @controller.url_for(options) + options = options.symbolize_keys + + [options[:action], options[:id].to_param].compact.join('/') + end + end + + def test_generic_input_tag + assert_dom_equal( + %(), input("post", "title") + ) + end + + def test_text_area_with_errors + assert_dom_equal( + %(
), + text_area("post", "body") + ) + end + + def test_text_field_with_errors + assert_dom_equal( + %(
), + text_field("post", "author_name") + ) + end + + def test_form_with_string + assert_dom_equal( + %(


\n


), + form("post") + ) + + silence_warnings do + class << @post + def new_record?() false end + def to_param() id end + def id() 1 end + end + end + + assert_dom_equal( + %(


\n


), + form("post") + ) + end + + def test_form_with_protect_against_forgery + @protect_against_forgery = true + @request_forgery_protection_token = 'authenticity_token' + @form_authenticity_token = '123' + assert_dom_equal( + %(


\n


), + form("post") + ) + end + + def test_form_with_method_option + assert_dom_equal( + %(


\n


), + form("post", :method=>'get') + ) + end + + def test_form_with_action_option + output_buffer << form("post", :action => "sign") + assert_select "form[action=sign]" do |form| + assert_select "input[type=submit][value=Sign]" + end + end + + def test_form_with_date + silence_warnings do + def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end + end + + assert_dom_equal( + %(


\n\n\n

), + form("post") + ) + end + + def test_form_with_datetime + silence_warnings do + def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end + end + @post.written_on = Time.gm(2004, 6, 15, 16, 30) + + assert_dom_equal( + %(


\n\n\n — \n : \n

), + form("post") + ) + end + + def test_error_for_block + assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post") + assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1") + assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => nil, :id => "errorDeathById", :header_tag => "h1") + assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => "errorDeathByClass", :id => nil, :header_tag => "h1") + end + + def test_error_messages_for_escapes_html + @dirty_post = DirtyPost.new + assert_dom_equal %(

1 error prohibited this dirty post from being saved

There were problems with the following fields:

  • Author name can't be <em>empty</em>
), error_messages_for("dirty_post") + end + + def test_error_messages_for_handles_nil + assert_equal "", error_messages_for("notthere") + end + + def test_error_message_on_escapes_html + @dirty_post = DirtyPost.new + assert_dom_equal "
can't be <em>empty</em>
", error_message_on(:dirty_post, :author_name) + end + + def test_error_message_on_handles_nil + assert_equal "", error_message_on("notthere", "notthere") + end + + def test_error_message_on + assert_dom_equal "
can't be empty
", error_message_on(:post, :author_name) + end + + def test_error_message_on_no_instance_variable + other_post = @post + assert_dom_equal "
can't be empty
", error_message_on(other_post, :author_name) + end + + def test_error_message_on_with_options_hash + assert_dom_equal "
beforecan't be emptyafter
", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after') + end + + def test_error_messages_for_many_objects + assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
  • User email can't be empty
), error_messages_for("post", "user") + + # reverse the order, error order changes and so does the title + assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post") + + # add the default to put post back in the title + assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object_name => "post") + + # symbols work as well + assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => :post) + + # any default works too + assert_dom_equal %(

2 errors prohibited this monkey from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => "monkey") + + # should space object name + assert_dom_equal %(

2 errors prohibited this chunky bacon from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => "chunky_bacon") + + # hide header and explanation messages with nil or empty string + assert_dom_equal %(
  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :header_message => nil, :message => "") + + # override header and explanation messages + header_message = "Yikes! Some errors" + message = "Please fix the following fields and resubmit:" + assert_dom_equal %(

#{header_message}

#{message}

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :header_message => header_message, :message => message) + end + + def test_error_messages_for_non_instance_variable + actual_user = @user + actual_post = @post + @user = nil + @post = nil + + #explicitly set object + assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :object => actual_post) + + #multiple objects + assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object => [actual_user, actual_post]) + + #nil object + assert_equal '', error_messages_for('user', :object => nil) + end + + def test_error_messages_for_model_objects + error = error_messages_for(@post) + assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), + error + + error = error_messages_for(@user, @post) + assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), + error + end + + def test_form_with_string_multipart + assert_dom_equal( + %(


\n


), + form("post", :multipart => true) + ) + end +end diff --git a/actionpack/test/template/active_record_helper_i18n_test.rb b/actionpack/test/template/active_record_helper_i18n_test.rb deleted file mode 100644 index 047f81be29..0000000000 --- a/actionpack/test/template/active_record_helper_i18n_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'abstract_unit' - -class ActiveRecordHelperI18nTest < Test::Unit::TestCase - include ActionView::Context - include ActionView::Helpers::ActiveModelHelper - - attr_reader :request - - def setup - @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages']) - @object.stubs :to_model => @object - @object.stubs :class => stub(:model_name => stub(:human => "")) - - @object_name = 'book_seller' - @object_name_without_underscore = 'book seller' - - stubs(:content_tag).returns 'content_tag' - - I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved" - I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:activerecord, :errors, :template]).returns 'There were problems with the following fields:' - end - - def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message - I18n.expects(:translate).with(:'header', :locale => 'en', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').never - error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en') - end - - def test_error_messages_for_given_no_header_option_it_translates_header_message - I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:activerecord, :errors, :template], :count => 1, :model => '').returns 'header message' - I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns '' - error_messages_for(:object => @object, :locale => 'en') - end - - def test_error_messages_for_given_a_message_option_it_does_not_translate_message - I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activerecord, :errors, :template]).never - I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns '' - error_messages_for(:object => @object, :message => 'message', :locale => 'en') - end - - def test_error_messages_for_given_no_message_option_it_translates_message - I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:activerecord, :errors, :template]).returns 'There were problems with the following fields:' - I18n.expects(:t).with('', :default => '', :count => 1, :scope => [:activerecord, :models]).once.returns '' - error_messages_for(:object => @object, :locale => 'en') - end - - def test_error_messages_for_given_object_name_it_translates_object_name - I18n.expects(:t).with(:header, :locale => 'en', :scope => [:activerecord, :errors, :template], :count => 1, :model => @object_name_without_underscore).returns "1 error prohibited this #{@object_name_without_underscore} from being saved" - I18n.expects(:t).with(@object_name, :default => @object_name_without_underscore, :count => 1, :scope => [:activerecord, :models]).once.returns @object_name_without_underscore - error_messages_for(:object => @object, :locale => 'en', :object_name => @object_name) - end -end diff --git a/actionpack/test/template/active_record_helper_test.rb b/actionpack/test/template/active_record_helper_test.rb deleted file mode 100644 index c149070f2a..0000000000 --- a/actionpack/test/template/active_record_helper_test.rb +++ /dev/null @@ -1,314 +0,0 @@ -require 'abstract_unit' - -class ActiveRecordHelperTest < ActionView::TestCase - tests ActionView::Helpers::ActiveModelHelper - - silence_warnings do - class Post < Struct.new(:title, :author_name, :body, :secret, :written_on) - extend ActiveModel::Naming - include ActiveModel::Conversion - end - - class User < Struct.new(:email) - extend ActiveModel::Naming - include ActiveModel::Conversion - end - - class Column < Struct.new(:type, :name, :human_name) - extend ActiveModel::Naming - include ActiveModel::Conversion - end - end - - class DirtyPost - class Errors - def empty? - false - end - - def count - 1 - end - - def full_messages - ["Author name can't be empty"] - end - - def [](field) - ["can't be empty"] - end - end - - def errors - Errors.new - end - end - - def setup_post - @post = Post.new - def @post.errors - Class.new { - def [](field) - case field.to_s - when "author_name" - ["can't be empty"] - when "body" - ['foo'] - else - [] - end - end - def empty?() false end - def count() 1 end - def full_messages() [ "Author name can't be empty" ] end - }.new - end - - def @post.new_record?() true end - def @post.to_param() nil end - - def @post.column_for_attribute(attr_name) - Post.content_columns.select { |column| column.name == attr_name }.first - end - - silence_warnings do - def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end - end - - @post.title = "Hello World" - @post.author_name = "" - @post.body = "Back to the hill and over it again!" - @post.secret = 1 - @post.written_on = Date.new(2004, 6, 15) - end - - def setup_user - @user = User.new - def @user.errors - Class.new { - def [](field) field == "email" ? ['nonempty'] : [] end - def empty?() false end - def count() 1 end - def full_messages() [ "User email can't be empty" ] end - }.new - end - - def @user.new_record?() true end - def @user.to_param() nil end - - def @user.column_for_attribute(attr_name) - User.content_columns.select { |column| column.name == attr_name }.first - end - - silence_warnings do - def User.content_columns() [ Column.new(:string, "email", "Email") ] end - end - - @user.email = "" - end - - def protect_against_forgery? - @protect_against_forgery ? true : false - end - attr_accessor :request_forgery_protection_token, :form_authenticity_token - - def setup - super - setup_post - setup_user - - @response = ActionController::TestResponse.new - - @controller = Object.new - def @controller.url_for(options) - options = options.symbolize_keys - - [options[:action], options[:id].to_param].compact.join('/') - end - end - - def test_generic_input_tag - assert_dom_equal( - %(), input("post", "title") - ) - end - - def test_text_area_with_errors - assert_dom_equal( - %(
), - text_area("post", "body") - ) - end - - def test_text_field_with_errors - assert_dom_equal( - %(
), - text_field("post", "author_name") - ) - end - - def test_form_with_string - assert_dom_equal( - %(


\n


), - form("post") - ) - - silence_warnings do - class << @post - def new_record?() false end - def to_param() id end - def id() 1 end - end - end - - assert_dom_equal( - %(


\n


), - form("post") - ) - end - - def test_form_with_protect_against_forgery - @protect_against_forgery = true - @request_forgery_protection_token = 'authenticity_token' - @form_authenticity_token = '123' - assert_dom_equal( - %(


\n


), - form("post") - ) - end - - def test_form_with_method_option - assert_dom_equal( - %(


\n


), - form("post", :method=>'get') - ) - end - - def test_form_with_action_option - output_buffer << form("post", :action => "sign") - assert_select "form[action=sign]" do |form| - assert_select "input[type=submit][value=Sign]" - end - end - - def test_form_with_date - silence_warnings do - def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end - end - - assert_dom_equal( - %(


\n\n\n

), - form("post") - ) - end - - def test_form_with_datetime - silence_warnings do - def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end - end - @post.written_on = Time.gm(2004, 6, 15, 16, 30) - - assert_dom_equal( - %(


\n\n\n — \n : \n

), - form("post") - ) - end - - def test_error_for_block - assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post") - assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1") - assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => nil, :id => "errorDeathById", :header_tag => "h1") - assert_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :class => "errorDeathByClass", :id => nil, :header_tag => "h1") - end - - def test_error_messages_for_escapes_html - @dirty_post = DirtyPost.new - assert_dom_equal %(

1 error prohibited this dirty post from being saved

There were problems with the following fields:

  • Author name can't be <em>empty</em>
), error_messages_for("dirty_post") - end - - def test_error_messages_for_handles_nil - assert_equal "", error_messages_for("notthere") - end - - def test_error_message_on_escapes_html - @dirty_post = DirtyPost.new - assert_dom_equal "
can't be <em>empty</em>
", error_message_on(:dirty_post, :author_name) - end - - def test_error_message_on_handles_nil - assert_equal "", error_message_on("notthere", "notthere") - end - - def test_error_message_on - assert_dom_equal "
can't be empty
", error_message_on(:post, :author_name) - end - - def test_error_message_on_no_instance_variable - other_post = @post - assert_dom_equal "
can't be empty
", error_message_on(other_post, :author_name) - end - - def test_error_message_on_with_options_hash - assert_dom_equal "
beforecan't be emptyafter
", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after') - end - - def test_error_messages_for_many_objects - assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
  • User email can't be empty
), error_messages_for("post", "user") - - # reverse the order, error order changes and so does the title - assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post") - - # add the default to put post back in the title - assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object_name => "post") - - # symbols work as well - assert_dom_equal %(

2 errors prohibited this post from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => :post) - - # any default works too - assert_dom_equal %(

2 errors prohibited this monkey from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => "monkey") - - # should space object name - assert_dom_equal %(

2 errors prohibited this chunky bacon from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :object_name => "chunky_bacon") - - # hide header and explanation messages with nil or empty string - assert_dom_equal %(
  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :header_message => nil, :message => "") - - # override header and explanation messages - header_message = "Yikes! Some errors" - message = "Please fix the following fields and resubmit:" - assert_dom_equal %(

#{header_message}

#{message}

  • User email can't be empty
  • Author name can't be empty
), error_messages_for(:user, :post, :header_message => header_message, :message => message) - end - - def test_error_messages_for_non_instance_variable - actual_user = @user - actual_post = @post - @user = nil - @post = nil - - #explicitly set object - assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), error_messages_for("post", :object => actual_post) - - #multiple objects - assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), error_messages_for("user", "post", :object => [actual_user, actual_post]) - - #nil object - assert_equal '', error_messages_for('user', :object => nil) - end - - def test_error_messages_for_model_objects - error = error_messages_for(@post) - assert_dom_equal %(

1 error prohibited this post from being saved

There were problems with the following fields:

  • Author name can't be empty
), - error - - error = error_messages_for(@user, @post) - assert_dom_equal %(

2 errors prohibited this user from being saved

There were problems with the following fields:

  • User email can't be empty
  • Author name can't be empty
), - error - end - - def test_form_with_string_multipart - assert_dom_equal( - %(


\n


), - form("post", :multipart => true) - ) - end -end -- cgit v1.2.3 From d50413826f1892b5d1250f578b283d52b85b4f6c Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 16:36:39 -0500 Subject: Invalid route requirements should always raise an exception even if they are unused --- actionpack/test/controller/routing_test.rb | 8 -------- 1 file changed, 8 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 7c88520bac..cd13f36681 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1019,14 +1019,6 @@ class RouteSetTest < ActiveSupport::TestCase map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+\z/ end end - assert_nothing_raised do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/, :name => /^(david|jamis)/ - end - assert_raise ActionController::RoutingError do - set.generate :controller => 'pages', :action => 'show', :id => 10 - end - end end def test_route_requirements_with_invalid_http_method_is_invalid -- cgit v1.2.3 From 20f0b33035aeb1ca8ca7bdfb98371b58c6fd10c1 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 16:39:16 -0500 Subject: Drop support for routing :generate_all --- actionpack/test/controller/routing_test.rb | 14 -------------- 1 file changed, 14 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index cd13f36681..345039f5b2 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1407,20 +1407,6 @@ class RouteSetTest < ActiveSupport::TestCase {:controller => 'post', :action => 'show', :project_id => '1'}) end - def test_generate_all - set.draw do |map| - map.connect 'show_post/:id', :controller => 'post', :action => 'show' - map.connect ':controller/:action/:id' - end - all = set.generate( - {:action => 'show', :id => 10, :generate_all => true}, - {:controller => 'post', :action => 'show'} - ) - assert_equal 2, all.length - assert_equal '/show_post/10', all.first - assert_equal '/post/show/10', all.last - end - def test_named_route_in_nested_resource set.draw do |map| map.resources :projects do |project| -- cgit v1.2.3 From e00f57e20833a297efd1670890ebe5b030dbfdf1 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 16:43:17 -0500 Subject: No longer need this special routing exception message because these kinds of ambiguous generations are deprecated --- actionpack/test/controller/routing_test.rb | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 345039f5b2..7aced7b6f5 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -425,19 +425,6 @@ class LegacyRouteSetTests < Test::Unit::TestCase assert_no_match /:controller=>"post"/, diff_match end - # this specifies the case where your formerly would get a very confusing error message with an empty diff - def test_should_have_better_error_message_when_options_diff_is_empty - rs.draw do |map| - map.content '/content/:query', :controller => 'content', :action => 'show' - end - - exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'content', :action => 'show', :use_route => "content") } - assert_match %r[:action=>"show"], exception.message - assert_match %r[:controller=>"content"], exception.message - assert_match %r[you may have ambiguous routes, or you may need to supply additional parameters for this route], exception.message - assert_match %r[content_url has the following required parameters: \["content", :query\] - are they all satisfied?], exception.message - end - def test_dynamic_path_allowed rs.draw do |map| map.connect '*path', :controller => 'content', :action => 'show_file' -- cgit v1.2.3 From 6c2a73909ec71148cec60c46e11d57d40247bb63 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 17:17:23 -0500 Subject: Stop using with_controllers in tests --- actionpack/test/controller/routing_test.rb | 168 +++++++++++++---------------- 1 file changed, 72 insertions(+), 96 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 7aced7b6f5..1ceb1ab68c 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -747,12 +747,9 @@ class RouteSetTest < ActiveSupport::TestCase def default_route_set @default_route_set ||= begin - set = nil - ActionController::Routing.with_controllers(['accounts']) do - set = ROUTING::RouteSet.new - set.draw do |map| - map.connect '/:controller/:action/:id/' - end + set = ROUTING::RouteSet.new + set.draw do |map| + map.connect '/:controller/:action/:id/' end set end @@ -940,44 +937,38 @@ class RouteSetTest < ActiveSupport::TestCase end def test_draw_default_route - ActionController::Routing.with_controllers(['users']) do - set.draw do |map| - map.connect '/:controller/:action/:id' - end + set.draw do |map| + map.connect '/:controller/:action/:id' + end - assert_equal 1, set.routes.size + assert_equal 1, set.routes.size - assert_equal '/users/show/10', set.generate(:controller => 'users', :action => 'show', :id => 10) - assert_equal '/users/index/10', set.generate(:controller => 'users', :id => 10) + assert_equal '/users/show/10', set.generate(:controller => 'users', :action => 'show', :id => 10) + assert_equal '/users/index/10', set.generate(:controller => 'users', :id => 10) - assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10')) - assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10/')) - end + assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10')) + assert_equal({:controller => 'users', :action => 'index', :id => '10'}, set.recognize_path('/users/index/10/')) end def test_draw_default_route_with_default_controller - ActionController::Routing.with_controllers(['users']) do - set.draw do |map| - map.connect '/:controller/:action/:id', :controller => 'users' - end - assert_equal({:controller => 'users', :action => 'index'}, set.recognize_path('/')) + set.draw do |map| + map.connect '/:controller/:action/:id', :controller => 'users' end + assert_equal({:controller => 'users', :action => 'index'}, set.recognize_path('/')) end def test_route_with_parameter_shell - ActionController::Routing.with_controllers(['users', 'pages']) do - set.draw do |map| - map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ - map.connect '/:controller/:action/:id' - end + set.draw do |map| + map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ + map.connect '/:controller/:action/:id' + end - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index')) - assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list')) + assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) + assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index')) + assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) - end + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) end def test_route_requirements_with_anchor_chars_are_invalid @@ -1568,101 +1559,86 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/named')) end - def test_interpolation_chunk_should_respect_raw - ActionController::Routing.with_controllers(['hello']) do - set.draw do |map| - map.connect '/Hello World', :controller => 'hello' - end - - assert_equal '/Hello%20World', set.generate(:controller => 'hello') - assert_equal({:controller => "hello", :action => "index"}, set.recognize_path('/Hello World')) - assert_raise(ActionController::RoutingError) { set.recognize_path('/Hello%20World') } + set.draw do |map| + map.connect '/Hello World', :controller => 'hello' end + + assert_equal '/Hello%20World', set.generate(:controller => 'hello') + assert_equal({:controller => "hello", :action => "index"}, set.recognize_path('/Hello World')) + assert_raise(ActionController::RoutingError) { set.recognize_path('/Hello%20World') } end def test_value_should_not_be_double_unescaped - ActionController::Routing.with_controllers(['foo']) do - set.draw do |map| - map.connect '/Карта', :controller => 'foo' - end - - assert_equal '/%D0%9A%D0%B0%D1%80%D1%82%D0%B0', set.generate(:controller => 'foo') - assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Карта')) - assert_raise(ActionController::RoutingError) { set.recognize_path('/%D0%9A%D0%B0%D1%80%D1%82%D0%B0') } + set.draw do |map| + map.connect '/Карта', :controller => 'foo' end + + assert_equal '/%D0%9A%D0%B0%D1%80%D1%82%D0%B0', set.generate(:controller => 'foo') + assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Карта')) + assert_raise(ActionController::RoutingError) { set.recognize_path('/%D0%9A%D0%B0%D1%80%D1%82%D0%B0') } end def test_regexp_chunk_should_escape_specials - ActionController::Routing.with_controllers(['foo', 'bar']) do - set.draw do |map| - map.connect '/Hello*World', :controller => 'foo' - map.connect '/HelloWorld', :controller => 'bar' - end + set.draw do |map| + map.connect '/Hello*World', :controller => 'foo' + map.connect '/HelloWorld', :controller => 'bar' + end - assert_equal '/Hello*World', set.generate(:controller => 'foo') - assert_equal '/HelloWorld', set.generate(:controller => 'bar') + assert_equal '/Hello*World', set.generate(:controller => 'foo') + assert_equal '/HelloWorld', set.generate(:controller => 'bar') - assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Hello*World')) - assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/HelloWorld')) - end + assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Hello*World')) + assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/HelloWorld')) end def test_regexp_chunk_should_add_question_mark_for_optionals - ActionController::Routing.with_controllers(['foo', 'bar']) do - set.draw do |map| - map.connect '/', :controller => 'foo' - map.connect '/hello', :controller => 'bar' - end + set.draw do |map| + map.connect '/', :controller => 'foo' + map.connect '/hello', :controller => 'bar' + end - assert_equal '/', set.generate(:controller => 'foo') - assert_equal '/hello', set.generate(:controller => 'bar') + assert_equal '/', set.generate(:controller => 'foo') + assert_equal '/hello', set.generate(:controller => 'bar') - assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/')) - assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/hello')) - end + assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/')) + assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/hello')) end def test_assign_route_options_with_anchor_chars - ActionController::Routing.with_controllers(['cars']) do - set.draw do |map| - map.connect '/cars/:action/:person/:car/', :controller => 'cars' - end + set.draw do |map| + map.connect '/cars/:action/:person/:car/', :controller => 'cars' + end - assert_equal '/cars/buy/1/2', set.generate(:controller => 'cars', :action => 'buy', :person => '1', :car => '2') + assert_equal '/cars/buy/1/2', set.generate(:controller => 'cars', :action => 'buy', :person => '1', :car => '2') - assert_equal({:controller => "cars", :action => "buy", :person => "1", :car => "2"}, set.recognize_path('/cars/buy/1/2')) - end + assert_equal({:controller => "cars", :action => "buy", :person => "1", :car => "2"}, set.recognize_path('/cars/buy/1/2')) end def test_segmentation_of_dot_path - ActionController::Routing.with_controllers(['books']) do - set.draw do |map| - map.connect '/books/:action.rss', :controller => 'books' - end + set.draw do |map| + map.connect '/books/:action.rss', :controller => 'books' + end - assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list') + assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list') - assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list.rss')) - end + assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list.rss')) end def test_segmentation_of_dynamic_dot_path - ActionController::Routing.with_controllers(['books']) do - set.draw do |map| - map.connect '/books/:action.:format', :controller => 'books' - end + set.draw do |map| + map.connect '/books/:action.:format', :controller => 'books' + end - assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list', :format => 'rss') - assert_equal '/books/list.xml', set.generate(:controller => 'books', :action => 'list', :format => 'xml') - assert_equal '/books/list', set.generate(:controller => 'books', :action => 'list') - assert_equal '/books', set.generate(:controller => 'books', :action => 'index') + assert_equal '/books/list.rss', set.generate(:controller => 'books', :action => 'list', :format => 'rss') + assert_equal '/books/list.xml', set.generate(:controller => 'books', :action => 'list', :format => 'xml') + assert_equal '/books/list', set.generate(:controller => 'books', :action => 'list') + assert_equal '/books', set.generate(:controller => 'books', :action => 'index') - assert_equal({:controller => "books", :action => "list", :format => "rss"}, set.recognize_path('/books/list.rss')) - assert_equal({:controller => "books", :action => "list", :format => "xml"}, set.recognize_path('/books/list.xml')) - assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list')) - assert_equal({:controller => "books", :action => "index"}, set.recognize_path('/books')) - end + assert_equal({:controller => "books", :action => "list", :format => "rss"}, set.recognize_path('/books/list.rss')) + assert_equal({:controller => "books", :action => "list", :format => "xml"}, set.recognize_path('/books/list.xml')) + assert_equal({:controller => "books", :action => "list"}, set.recognize_path('/books/list')) + assert_equal({:controller => "books", :action => "index"}, set.recognize_path('/books')) end def test_slashes_are_implied -- cgit v1.2.3 From 702df0d2384609f1b0ee1cf3f068c823dc1b3a0d Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 17:26:50 -0500 Subject: Fix standalone run of routing test. Only reference controllers that are provided in fake_controllers --- actionpack/test/controller/routing_test.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 1ceb1ab68c..5f160c1464 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1345,20 +1345,20 @@ class RouteSetTest < ActiveSupport::TestCase set.draw do |map| map.connect ':controller/:action/:id' end - assert_equal '/post', set.generate( - {:controller => 'post', :action => 'index'}, - {:controller => 'post', :action => 'show', :id => '10'} + assert_equal '/books', set.generate( + {:controller => 'books', :action => 'index'}, + {:controller => 'books', :action => 'show', :id => '10'} ) end def test_query_params_will_be_shown_when_recalled set.draw do |map| - map.connect 'show_post/:parameter', :controller => 'post', :action => 'show' + map.connect 'show_weblog/:parameter', :controller => 'weblog', :action => 'show' map.connect ':controller/:action/:id' end - assert_equal '/post/edit?parameter=1', set.generate( + assert_equal '/weblog/edit?parameter=1', set.generate( {:action => 'edit', :parameter => 1}, - {:controller => 'post', :action => 'show', :parameter => 1} + {:controller => 'weblog', :action => 'show', :parameter => 1} ) end @@ -1380,9 +1380,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_expiry_determination_should_consider_values_with_to_param set.draw { |map| map.connect 'projects/:project_id/:controller/:action' } - assert_equal '/projects/1/post/show', set.generate( + assert_equal '/projects/1/weblog/show', set.generate( {:action => 'show', :project_id => 1}, - {:controller => 'post', :action => 'show', :project_id => '1'}) + {:controller => 'weblog', :action => 'show', :project_id => '1'}) end def test_named_route_in_nested_resource -- cgit v1.2.3 From e900a8437a6f1dcbf993dfbb1b82ee51a11128b4 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 17:34:00 -0500 Subject: Fix brittle query string comparisons --- actionpack/test/controller/routing_test.rb | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 5f160c1464..def5ff4957 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1723,32 +1723,43 @@ class RouteSetTest < ActiveSupport::TestCase end def test_build_empty_query_string - assert_equal '/foo', default_route_set.generate({:controller => 'foo'}) + assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo'}) end def test_build_query_string_with_nil_value - assert_equal '/foo', default_route_set.generate({:controller => 'foo', :x => nil}) + assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo', :x => nil}) end def test_simple_build_query_string - assert_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => '1', :y => '2'}) + assert_uri_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => '1', :y => '2'}) end def test_convert_ints_build_query_string - assert_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => 1, :y => 2}) + assert_uri_equal '/foo?x=1&y=2', default_route_set.generate({:controller => 'foo', :x => 1, :y => 2}) end def test_escape_spaces_build_query_string - assert_equal '/foo?x=hello+world&y=goodbye+world', default_route_set.generate({:controller => 'foo', :x => 'hello world', :y => 'goodbye world'}) + assert_uri_equal '/foo?x=hello+world&y=goodbye+world', default_route_set.generate({:controller => 'foo', :x => 'hello world', :y => 'goodbye world'}) end def test_expand_array_build_query_string - assert_equal '/foo?x%5B%5D=1&x%5B%5D=2', default_route_set.generate({:controller => 'foo', :x => [1, 2]}) + assert_uri_equal '/foo?x%5B%5D=1&x%5B%5D=2', default_route_set.generate({:controller => 'foo', :x => [1, 2]}) end def test_escape_spaces_build_query_string_selected_keys - assert_equal '/foo?x=hello+world', default_route_set.generate({:controller => 'foo', :x => 'hello world'}) + assert_uri_equal '/foo?x=hello+world', default_route_set.generate({:controller => 'foo', :x => 'hello world'}) end + + private + def assert_uri_equal(expected, actual) + assert_equal(sort_query_string_params(expected), sort_query_string_params(actual)) + end + + def sort_query_string_params(uri) + path, qs = uri.split('?') + qs = qs.split('&').sort.join('&') if qs + qs ? "#{path}?#{qs}" : path + end end class RouteLoadingTest < Test::Unit::TestCase -- cgit v1.2.3 From cc0103fe833d556e750fd040e34cf0165c3c7ccc Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 19:18:18 -0500 Subject: Fix brittle query string comparisons --- actionpack/test/controller/url_rewriter_test.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index 4c4bf9ade4..d81ced96a8 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -321,8 +321,8 @@ class UrlWriterTests < ActionController::TestCase params = extract_params(url) assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query - assert_equal params[2], { 'query[person][position][]' => 'prof' }.to_query - assert_equal params[3], { 'query[person][position][]' => 'art director' }.to_query + assert_equal params[2], { 'query[person][position][]' => 'art director' }.to_query + assert_equal params[3], { 'query[person][position][]' => 'prof' }.to_query end def test_path_generation_for_symbol_parameter_keys @@ -359,10 +359,10 @@ class UrlWriterTests < ActionController::TestCase controller = kls.new params = {:id => 1, :format => :xml} assert_deprecated do - assert_equal("/posts/1.xml", controller.send(:formatted_post_path, params)) + assert_equal("/posts/1.xml", controller.send(:formatted_post_path, params)) end assert_deprecated do - assert_equal("/posts/1.xml", controller.send(:formatted_post_path, 1, :xml)) + assert_equal("/posts/1.xml", controller.send(:formatted_post_path, 1, :xml)) end end end @@ -382,6 +382,6 @@ class UrlWriterTests < ActionController::TestCase private def extract_params(url) - url.split('?', 2).last.split('&') + url.split('?', 2).last.split('&').sort end end -- cgit v1.2.3 From f1767c1513172415d53f8aa347d9936f85560128 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 21:21:34 -0500 Subject: Drop legacy support for case insensitive controller recognition --- actionpack/test/controller/routing_test.rb | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index def5ff4957..3b70e7162c 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -490,17 +490,6 @@ class LegacyRouteSetTests < Test::Unit::TestCase assert_equal '/content', rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) end - def test_recognition_with_uppercase_controller_name - @rs.draw {|m| m.connect ':controller/:action/:id' } - assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/Content")) - assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/ConTent/list")) - assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/CONTENT/show/10")) - - # these used to work, before the routes rewrite, but support for this was pulled in the new version... - #assert_equal({'controller' => "admin/news_feed", 'action' => 'index'}, rs.recognize_path("Admin/NewsFeed")) - #assert_equal({'controller' => "admin/news_feed", 'action' => 'index'}, rs.recognize_path("Admin/News_Feed")) - end - def test_requirement_should_prevent_optional_id rs.draw do |map| map.post 'post/:id', :controller=> 'post', :action=> 'show', :requirements => {:id => /\d+/} -- cgit v1.2.3 From 2c3ca9ae80c0ec30ce1aede77c4dafc55bf2957e Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 17 Oct 2009 21:56:43 -0500 Subject: This is routing error message test is tightly coupled to the implementation. Just test that it raises an exception. --- actionpack/test/controller/routing_test.rb | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 3b70e7162c..83b26ba5a9 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -407,22 +407,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase rs.draw do |map| map.post 'post/:id', :controller=> 'post', :action=> 'show', :requirements => {:id => /\d+/} end - exception = assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post") } - assert_match /^post_url failed to generate/, exception.message - from_match = exception.message.match(/from \{[^\}]+\}/).to_s - assert_match /:bad_param=>"foo"/, from_match - assert_match /:action=>"show"/, from_match - assert_match /:controller=>"post"/, from_match - - expected_match = exception.message.match(/expected: \{[^\}]+\}/).to_s - assert_no_match /:bad_param=>"foo"/, expected_match - assert_match /:action=>"show"/, expected_match - assert_match /:controller=>"post"/, expected_match - - diff_match = exception.message.match(/diff: \{[^\}]+\}/).to_s - assert_match /:bad_param=>"foo"/, diff_match - assert_no_match /:action=>"show"/, diff_match - assert_no_match /:controller=>"post"/, diff_match + assert_raise(ActionController::RoutingError) { rs.generate(:controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post") } end def test_dynamic_path_allowed -- cgit v1.2.3 From 6873b1d6589c0e8f1c29f156fb1841e165f3a127 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sun, 18 Oct 2009 00:26:36 -0500 Subject: Don't use use_controllers in routing tests --- actionpack/test/controller/routing_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 83b26ba5a9..8a9862a287 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -22,7 +22,6 @@ end # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < Test::Unit::TestCase def setup - ActionController::Routing.use_controllers! ['controller'] @set = ActionController::Routing::RouteSet.new @set.draw do |map| map.connect ':controller/:action/:variable/*additional' @@ -36,8 +35,9 @@ class UriReservedCharactersRoutingTest < Test::Unit::TestCase end def test_route_generation_escapes_unsafe_path_characters - assert_equal "/contr#{@segment}oller/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2", - @set.generate(:controller => "contr#{@segment}oller", + @set.generate(:controller => "content", :action => "act#{@segment}ion", :variable => "variable", :additional => "foo") + assert_equal "/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2", + @set.generate(:controller => "content", :action => "act#{@segment}ion", :variable => "var#{@segment}iable", :additional => ["add#{@segment}itional-1", "add#{@segment}itional-2"]) @@ -52,7 +52,7 @@ class UriReservedCharactersRoutingTest < Test::Unit::TestCase end def test_route_generation_allows_passing_non_string_values_to_generated_helper - assert_equal "/controller/action/variable/1/2", @set.generate(:controller => "controller", + assert_equal "/content/action/variable/1/2", @set.generate(:controller => "content", :action => "action", :variable => "variable", :additional => [1, 2]) -- cgit v1.2.3 From 91726c201bf91b08dcb75f8d129d1002c489b79d Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sun, 18 Oct 2009 11:05:24 -0500 Subject: Relax generation requirements and only enforce the requirements used in the path segment --- actionpack/test/controller/routing_test.rb | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 8a9862a287..cbbd7e6951 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -997,19 +997,6 @@ class RouteSetTest < ActiveSupport::TestCase end end - def test_non_path_route_requirements_match_all - set.draw do |map| - map.connect 'page/37s', :controller => 'pages', :action => 'show', :name => /(jamis|david)/ - end - assert_equal '/page/37s', set.generate(:controller => 'pages', :action => 'show', :name => 'jamis') - assert_raise ActionController::RoutingError do - set.generate(:controller => 'pages', :action => 'show', :name => 'not_jamis') - end - assert_raise ActionController::RoutingError do - set.generate(:controller => 'pages', :action => 'show', :name => 'nor_jamis_and_david') - end - end - def test_recognize_with_encoded_id_and_regex set.draw do |map| map.connect 'page/:id', :controller => 'pages', :action => 'show', :id => /[a-zA-Z0-9\+]+/ -- cgit v1.2.3 From 418ce487c4a41df2c3a3c1485107f32eb94fa493 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 19 Oct 2009 18:46:59 -0700 Subject: Only undef to_json if it's defined --- actionpack/lib/action_controller/metal/responder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index a16ed97131..c6e847ba0f 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -95,7 +95,7 @@ module ActionController #:nodoc: delegate :get?, :post?, :put?, :delete?, :to => :request # Undefine :to_json since it's defined on Object - undef_method :to_json + undef_method(:to_json) if method_defined?(:to_json) # Initializes a new responder an invoke the proper format. If the format is # not defined, call to_format. -- cgit v1.2.3 From c9cd10c4fa77aedb9116105d4d3c4d5684af4e1c Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 19 Oct 2009 19:00:17 -0700 Subject: Bundle arel for AR integration tests --- actionpack/Gemfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'actionpack') diff --git a/actionpack/Gemfile b/actionpack/Gemfile index c236169b20..b3ffeaecb6 100644 --- a/actionpack/Gemfile +++ b/actionpack/Gemfile @@ -6,13 +6,12 @@ gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git", :branch => "rack gem "rack-test", "~> 0.5.0" gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport") gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel") +gem "arel", :git => "git://github.com/rails/arel.git" gem "activerecord", "3.0.pre", :vendored_at => rails_root.join("activerecord") gem "erubis", "~> 2.6.0" -only :test do - gem "mocha" - gem "sqlite3-ruby" - gem "RedCloth" -end +gem "mocha" +gem "sqlite3-ruby" +gem "RedCloth" disable_system_gems -- cgit v1.2.3 From 33258d713a4bc20b71e92fd656c923a7b189cd33 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 19 Oct 2009 22:40:28 -0500 Subject: Fix brittle query string comparisons --- actionpack/test/template/url_helper_test.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index cec53e479c..bf0b4ad3a7 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -26,7 +26,7 @@ class UrlHelperTest < ActionView::TestCase assert_equal "http://www.example.com?a=b&c=d", url_for(:a => 'b', :c => 'd', :escape => true) assert_equal "http://www.example.com?a=b&c=d", url_for(:a => 'b', :c => 'd', :escape => false) end - + def test_url_for_escaping_is_safety_aware assert url_for(:a => 'b', :c => 'd', :escape => true).html_safe?, "escaped urls should be html_safe?" assert !url_for(:a => 'b', :c => 'd', :escape => false).html_safe?, "non-escaped urls shouldn't be safe" @@ -54,7 +54,7 @@ class UrlHelperTest < ActionView::TestCase path = @view.url_for(:controller => :cheeses, :foo => :bar, :baz => :quux) - assert_equal '/cheeses?baz=quux&foo=bar', path + assert_equal '/cheeses?baz=quux&foo=bar', sort_query_string_params(path) end # todo: missing test cases @@ -284,21 +284,21 @@ class UrlHelperTest < ActionView::TestCase assert current_page?({ :action => "show", :controller => "weblog" }) assert current_page?("http://www.example.com/weblog/show") end - + def test_current_page_ignoring_params @controller.request = RequestMock.new("http://www.example.com/weblog/show?order=desc&page=1") @controller.url = "http://www.example.com/weblog/show?order=desc&page=1" assert current_page?({ :action => "show", :controller => "weblog" }) assert current_page?("http://www.example.com/weblog/show") end - + def test_current_page_with_params_that_match @controller.request = RequestMock.new("http://www.example.com/weblog/show?order=desc&page=1") @controller.url = "http://www.example.com/weblog/show?order=desc&page=1" assert current_page?({ :action => "show", :controller => "weblog", :order => "desc", :page => "1" }) assert current_page?("http://www.example.com/weblog/show?order=desc&page=1") end - + def test_link_unless_current @controller.request = RequestMock.new("http://www.example.com/weblog/show") @controller.url = "http://www.example.com/weblog/show" @@ -378,10 +378,17 @@ class UrlHelperTest < ActionView::TestCase assert_dom_equal "", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") end - + def protect_against_forgery? false end + + private + def sort_query_string_params(uri) + path, qs = uri.split('?') + qs = qs.split('&').sort.join('&') if qs + qs ? "#{path}?#{qs}" : path + end end class UrlHelperController < ActionController::Base -- cgit v1.2.3 From 6c581f5fd57dc1706d5f674b4371c07d040c4151 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 19 Oct 2009 22:41:58 -0500 Subject: Kill routing tests expecting path to be unescaped. Most rack servers already escape PATH_INFO. --- actionpack/test/controller/routing_test.rb | 62 ++++++------------------------ 1 file changed, 12 insertions(+), 50 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index cbbd7e6951..67448e66b9 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -27,7 +27,7 @@ class UriReservedCharactersRoutingTest < Test::Unit::TestCase map.connect ':controller/:action/:variable/*additional' end - safe, unsafe = %w(: @ & = + $ , ;), %w(^ / ? # [ ]) + safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ]) hex = unsafe.map { |char| '%' + char.unpack('H2').first.upcase } @segment = "#{safe.join}#{unsafe.join}".freeze @@ -366,10 +366,6 @@ class LegacyRouteSetTests < Test::Unit::TestCase results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F" assert results, "Recognition should have succeeded" assert_equal ['hello world', 'how are you?'], results[:path] - - results = rs.recognize_path "/file" - assert results, "Recognition should have succeeded" - assert_equal [], results[:path] end def test_paths_slashes_unescaped_with_ordered_parameters @@ -379,7 +375,7 @@ class LegacyRouteSetTests < Test::Unit::TestCase # No / to %2F in URI, only for query params. x = setup_for_named_route - assert_equal("/file/hello/world", x.send(:path_path, 'hello/world')) + assert_equal("/file/hello/world", x.send(:path_path, ['hello', 'world'])) end def test_non_controllers_cannot_be_matched @@ -1234,16 +1230,16 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal "/foo/bar/baz/7", url end - def test_id_is_not_impossibly_sticky - set.draw do |map| - map.connect 'foo/:number', :controller => "people", :action => "index" - map.connect ':controller/:action/:id' - end - - url = set.generate({:controller => "people", :action => "index", :number => 3}, - {:controller => "people", :action => "index", :id => "21"}) - assert_equal "/foo/3", url - end + # def test_id_is_not_impossibly_sticky + # set.draw do |map| + # map.connect 'foo/:number', :controller => "people", :action => "index" + # map.connect ':controller/:action/:id' + # end + # + # url = set.generate({:controller => "people", :action => "index", :number => 3}, + # {:controller => "people", :action => "index", :id => "21"}) + # assert_equal "/foo/3", url + # end def test_id_is_sticky_when_it_ought_to_be set.draw do |map| @@ -1520,39 +1516,6 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal({:controller => 'pages', :action => 'show', :name => :as_symbol}, set.recognize_path('/named')) end - def test_interpolation_chunk_should_respect_raw - set.draw do |map| - map.connect '/Hello World', :controller => 'hello' - end - - assert_equal '/Hello%20World', set.generate(:controller => 'hello') - assert_equal({:controller => "hello", :action => "index"}, set.recognize_path('/Hello World')) - assert_raise(ActionController::RoutingError) { set.recognize_path('/Hello%20World') } - end - - def test_value_should_not_be_double_unescaped - set.draw do |map| - map.connect '/Карта', :controller => 'foo' - end - - assert_equal '/%D0%9A%D0%B0%D1%80%D1%82%D0%B0', set.generate(:controller => 'foo') - assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Карта')) - assert_raise(ActionController::RoutingError) { set.recognize_path('/%D0%9A%D0%B0%D1%80%D1%82%D0%B0') } - end - - def test_regexp_chunk_should_escape_specials - set.draw do |map| - map.connect '/Hello*World', :controller => 'foo' - map.connect '/HelloWorld', :controller => 'bar' - end - - assert_equal '/Hello*World', set.generate(:controller => 'foo') - assert_equal '/HelloWorld', set.generate(:controller => 'bar') - - assert_equal({:controller => "foo", :action => "index"}, set.recognize_path('/Hello*World')) - assert_equal({:controller => "bar", :action => "index"}, set.recognize_path('/HelloWorld')) - end - def test_regexp_chunk_should_add_question_mark_for_optionals set.draw do |map| map.connect '/', :controller => 'foo' @@ -1654,7 +1617,6 @@ class RouteSetTest < ActiveSupport::TestCase def test_default_route_should_uri_escape_pluses expected = { :controller => 'pages', :action => 'show', :id => 'hello world' } - assert_equal expected, default_route_set.recognize_path('/pages/show/hello world') assert_equal expected, default_route_set.recognize_path('/pages/show/hello%20world') assert_equal '/pages/show/hello%20world', default_route_set.generate(expected, expected) -- cgit v1.2.3 From a1df2590744ed126981dfd5b5709ff6fd5dc6476 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 19 Oct 2009 23:32:06 -0500 Subject: Replace decaying routing internals w/ rack-mount --- actionpack/Gemfile | 1 + actionpack/lib/action_controller.rb | 6 +- actionpack/lib/action_controller/routing.rb | 18 +- .../lib/action_controller/routing/builder.rb | 199 ---------- .../routing/generation/polymorphic_routes.rb | 210 ---------- .../routing/generation/url_rewriter.rb | 204 ---------- .../lib/action_controller/routing/optimisations.rb | 130 ------ .../routing/polymorphic_routes.rb | 210 ++++++++++ .../routing/recognition_optimisation.rb | 167 -------- actionpack/lib/action_controller/routing/route.rb | 267 ------------- .../lib/action_controller/routing/route_set.rb | 434 +++++++++++++++------ .../lib/action_controller/routing/routing_ext.rb | 4 - .../lib/action_controller/routing/segments.rb | 343 ---------------- .../lib/action_controller/routing/url_rewriter.rb | 204 ++++++++++ .../lib/action_controller/testing/test_case.rb | 9 +- actionpack/test/controller/routing_test.rb | 27 -- 16 files changed, 755 insertions(+), 1678 deletions(-) delete mode 100644 actionpack/lib/action_controller/routing/builder.rb delete mode 100644 actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb delete mode 100644 actionpack/lib/action_controller/routing/generation/url_rewriter.rb delete mode 100644 actionpack/lib/action_controller/routing/optimisations.rb create mode 100644 actionpack/lib/action_controller/routing/polymorphic_routes.rb delete mode 100644 actionpack/lib/action_controller/routing/recognition_optimisation.rb delete mode 100644 actionpack/lib/action_controller/routing/route.rb delete mode 100644 actionpack/lib/action_controller/routing/routing_ext.rb delete mode 100644 actionpack/lib/action_controller/routing/segments.rb create mode 100644 actionpack/lib/action_controller/routing/url_rewriter.rb (limited to 'actionpack') diff --git a/actionpack/Gemfile b/actionpack/Gemfile index b3ffeaecb6..af95766608 100644 --- a/actionpack/Gemfile +++ b/actionpack/Gemfile @@ -3,6 +3,7 @@ rails_root = Pathname.new(File.dirname(__FILE__)).join("..") Gem.sources.each { |uri| source uri } gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git", :branch => "rack-1.0" +gem "rack-mount", :git => "git://github.com/josh/rack-mount.git" gem "rack-test", "~> 0.5.0" gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport") gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel") diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 6702cb47f8..809e5f2ad4 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -26,14 +26,14 @@ module ActionController autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :MimeResponds, 'action_controller/metal/mime_responds' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes' + autoload :PolymorphicRoutes, 'action_controller/routing/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' autoload :Resources, 'action_controller/routing/resources' autoload :SessionManagement, 'action_controller/metal/session_management' autoload :TestCase, 'action_controller/testing/test_case' autoload :TestProcess, 'action_controller/testing/process' - autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter' - autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter' + autoload :UrlRewriter, 'action_controller/routing/url_rewriter' + autoload :UrlWriter, 'action_controller/routing/url_rewriter' autoload :Verification, 'action_controller/metal/verification' autoload :Flash, 'action_controller/metal/flash' diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index 5b9ded83dd..979a4ad8c9 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -1,16 +1,8 @@ -require 'cgi' -require 'uri' -require 'set' - -require 'active_support/core_ext/module/aliasing' -require 'active_support/core_ext/module/attribute_accessors' -require 'action_controller/routing/optimisations' -require 'action_controller/routing/routing_ext' -require 'action_controller/routing/route' -require 'action_controller/routing/segments' -require 'action_controller/routing/builder' +require 'active_support/core_ext/object/conversions' +require 'active_support/core_ext/boolean/conversions' +require 'active_support/core_ext/nil/conversions' +require 'active_support/core_ext/regexp' require 'action_controller/routing/route_set' -require 'action_controller/routing/recognition_optimisation' module ActionController # == Routing @@ -197,7 +189,7 @@ module ActionController # # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' # - # will glob all remaining parts of the route that were not recognized earlier. + # will glob all remaining parts of the route that were not recognized earlier. # The globbed values are in params[:path] as an array of path segments. # # == Route conditions diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb deleted file mode 100644 index 42ad12e1ea..0000000000 --- a/actionpack/lib/action_controller/routing/builder.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'active_support/core_ext/hash/except' - -module ActionController - module Routing - class RouteBuilder #:nodoc: - attr_reader :separators, :optional_separators - attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp - - def initialize - @separators = Routing::SEPARATORS - @optional_separators = %w( / ) - - @separator_regexp = /[#{Regexp.escape(separators.join)}]/ - @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/ - @interval_regexp = /(.*?)(#{separator_regexp}|$)/ - end - - # Accepts a "route path" (a string defining a route), and returns the array - # of segments that corresponds to it. Note that the segment array is only - # partially initialized--the defaults and requirements, for instance, need - # to be set separately, via the +assign_route_options+ method, and the - # optional? method for each segment will not be reliable until after - # +assign_route_options+ is called, as well. - def segments_for_route_path(path) - rest, segments = path, [] - - until rest.empty? - segment, rest = segment_for(rest) - segments << segment - end - segments - end - - # A factory method that returns a new segment instance appropriate for the - # format of the given string. - def segment_for(string) - segment = - case string - when /\A\.(:format)?\// - OptionalFormatSegment.new - when /\A:(\w+)/ - key = $1.to_sym - key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key) - when /\A\*(\w+)/ - PathSegment.new($1.to_sym, :optional => true) - when /\A\?(.*?)\?/ - StaticSegment.new($1, :optional => true) - when nonseparator_regexp - StaticSegment.new($1) - when separator_regexp - DividerSegment.new($&, :optional => optional_separators.include?($&)) - end - [segment, $~.post_match] - end - - # Split the given hash of options into requirement and default hashes. The - # segments are passed alongside in order to distinguish between default values - # and requirements. - def divide_route_options(segments, options) - options = options.except(:path_prefix, :name_prefix) - - if options[:namespace] - options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}" - end - - requirements = (options.delete(:requirements) || {}).dup - defaults = (options.delete(:defaults) || {}).dup - conditions = (options.delete(:conditions) || {}).dup - - validate_route_conditions(conditions) - - path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact - options.each do |key, value| - hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements - hash[key] = value - end - - [defaults, requirements, conditions] - end - - # Takes a hash of defaults and a hash of requirements, and assigns them to - # the segments. Any unused requirements (which do not correspond to a segment) - # are returned as a hash. - def assign_route_options(segments, defaults, requirements) - route_requirements = {} # Requirements that do not belong to a segment - - segment_named = Proc.new do |key| - segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } - end - - requirements.each do |key, requirement| - segment = segment_named[key] - if segment - raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - segment.regexp = requirement - else - route_requirements[key] = requirement - end - end - - defaults.each do |key, default| - segment = segment_named[key] - raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment - segment.is_optional = true - segment.default = default.to_param if default - end - - assign_default_route_options(segments) - ensure_required_segments(segments) - route_requirements - end - - # Assign default options, such as 'index' as a default for :action. This - # method must be run *after* user supplied requirements and defaults have - # been applied to the segments. - def assign_default_route_options(segments) - segments.each do |segment| - next unless segment.is_a? DynamicSegment - case segment.key - when :action - if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index' - segment.default ||= 'index' - segment.is_optional = true - end - when :id - if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ '' - segment.is_optional = true - end - end - end - end - - # Makes sure that there are no optional segments that precede a required - # segment. If any are found that precede a required segment, they are - # made required. - def ensure_required_segments(segments) - allow_optional = true - segments.reverse_each do |segment| - allow_optional &&= segment.optional? - if !allow_optional && segment.optional? - unless segment.optionality_implied? - warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." - end - segment.is_optional = false - elsif allow_optional && segment.respond_to?(:default) && segment.default - # if a segment has a default, then it is optional - segment.is_optional = true - end - end - end - - # Construct and return a route with the given path and options. - def build(path, options) - # Wrap the path with slashes - path = "/#{path}" unless path[0] == ?/ - path = "#{path}/" unless path[-1] == ?/ - - prefix = options[:path_prefix].to_s.gsub(/^\//,'') - path = "/#{prefix}#{path}" unless prefix.blank? - - segments = segments_for_route_path(path) - defaults, requirements, conditions = divide_route_options(segments, options) - requirements = assign_route_options(segments, defaults, requirements) - - # TODO: Segments should be frozen on initialize - segments.each { |segment| segment.freeze } - - route = Route.new(segments, requirements, conditions) - - if !route.significant_keys.include?(:controller) - raise ArgumentError, "Illegal route: the :controller must be specified!" - end - - route.freeze - end - - private - def validate_route_conditions(conditions) - if method = conditions[:method] - [method].flatten.each do |m| - if m == :head - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}" - end - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb b/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb deleted file mode 100644 index 2adf3575a7..0000000000 --- a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb +++ /dev/null @@ -1,210 +0,0 @@ -module ActionController - # Polymorphic URL helpers are methods for smart resolution to a named route call when - # given an Active Record model instance. They are to be used in combination with - # ActionController::Resources. - # - # These methods are useful when you want to generate correct URL or path to a RESTful - # resource without having to know the exact type of the record in question. - # - # Nested resources and/or namespaces are also supported, as illustrated in the example: - # - # polymorphic_url([:admin, @article, @comment]) - # - # results in: - # - # admin_article_comment_url(@article, @comment) - # - # == Usage within the framework - # - # Polymorphic URL helpers are used in a number of places throughout the Rails framework: - # - # * url_for, so you can use it with a record as the argument, e.g. - # url_for(@article); - # * ActionView::Helpers::FormHelper uses polymorphic_path, so you can write - # form_for(@article) without having to specify :url parameter for the form - # action; - # * redirect_to (which, in fact, uses url_for) so you can write - # redirect_to(post) in your controllers; - # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs - # for feed entries. - # - # == Prefixed polymorphic helpers - # - # In addition to polymorphic_url and polymorphic_path methods, a - # number of prefixed helpers are available as a shorthand to :action => "..." - # in options. Those are: - # - # * edit_polymorphic_url, edit_polymorphic_path - # * new_polymorphic_url, new_polymorphic_path - # - # Example usage: - # - # edit_polymorphic_path(@post) # => "/posts/1/edit" - # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" - module PolymorphicRoutes - # Constructs a call to a named RESTful route for the given record and returns the - # resulting URL string. For example: - # - # # calls post_url(post) - # polymorphic_url(post) # => "http://example.com/posts/1" - # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" - # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" - # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" - # polymorphic_url(Comment) # => "http://example.com/comments" - # - # ==== Options - # - # * :action - Specifies the action prefix for the named route: - # :new or :edit. Default is no prefix. - # * :routing_type - Allowed values are :path or :url. - # Default is :url. - # - # ==== Examples - # - # # an Article record - # polymorphic_url(record) # same as article_url(record) - # - # # a Comment record - # polymorphic_url(record) # same as comment_url(record) - # - # # it recognizes new records and maps to the collection - # record = Comment.new - # polymorphic_url(record) # same as comments_url() - # - # # the class of a record will also map to the collection - # polymorphic_url(Comment) # same as comments_url() - # - def polymorphic_url(record_or_hash_or_array, options = {}) - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 - end - - record = extract_record(record_or_hash_or_array) - record = record.to_model if record.respond_to?(:to_model) - namespace = extract_namespace(record_or_hash_or_array) - - args = case record_or_hash_or_array - when Hash; [ record_or_hash_or_array ] - when Array; record_or_hash_or_array.dup - else [ record_or_hash_or_array ] - end - - inflection = if options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:new_record?) && record.new_record?) || - (record.respond_to?(:destroyed?) && record.destroyed?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end - - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - - named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) - - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - __send__(named_route, *args) - end - - # Returns the path component of a URL for the given record. It uses - # polymorphic_url with :routing_type => :path. - def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) - end - - %w(edit new).each do |action| - module_eval <<-EOT, __FILE__, __LINE__ - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end - EOT - end - - def formatted_polymorphic_url(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) - options[:format] = record_or_hash.pop if Array === record_or_hash - polymorphic_url(record_or_hash, options) - end - - def formatted_polymorphic_path(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) - options[:format] = record_or_hash.pop if record_or_hash === Array - polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) - end - - private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' - end - - def routing_type(options) - options[:routing_type] || :url - end - - def build_named_route_call(records, namespace, inflection, options = {}) - unless records.is_a?(Array) - record = extract_record(records) - route = '' - else - record = records.pop - route = records.inject("") do |string, parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - string << "#{parent}_" - else - string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize - string << "_" - end - end - end - - if record.is_a?(Symbol) || record.is_a?(String) - route << "#{record}_" - else - route << "#{RecordIdentifier.__send__("plural_class_name", record)}" - route = route.singularize if inflection == :singular - route << "_" - end - - action_prefix(options) + namespace + route + routing_type(options).to_s - end - - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end - end - - # Remove the first symbols from the array and return the url prefix - # implied by those symbols. - def extract_namespace(record_or_hash_or_array) - return "" unless record_or_hash_or_array.is_a?(Array) - - namespace_keys = [] - while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) - namespace_keys << record_or_hash_or_array.shift - end - - namespace_keys.map {|k| "#{k}_"}.join - end - end -end diff --git a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb b/actionpack/lib/action_controller/routing/generation/url_rewriter.rb deleted file mode 100644 index 52b66c9303..0000000000 --- a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb +++ /dev/null @@ -1,204 +0,0 @@ -module ActionController - # In routes.rb one defines URL-to-controller mappings, but the reverse - # is also possible: an URL can be generated from one of your routing definitions. - # URL generation functionality is centralized in this module. - # - # See ActionController::Routing and ActionController::Resources for general - # information about routing and routes.rb. - # - # Tip: If you need to generate URLs from your models or some other place, - # then ActionController::UrlWriter is what you're looking for. Read on for - # an introduction. - # - # == URL generation from parameters - # - # As you may know, some functions - such as ActionController::Base#url_for - # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set - # of parameters. For example, you've probably had the chance to write code - # like this in one of your views: - # - # <%= link_to('Click here', :controller => 'users', - # :action => 'new', :message => 'Welcome!') %> - # - # #=> Generates a link to: /users/new?message=Welcome%21 - # - # link_to, and all other functions that require URL generation functionality, - # actually use ActionController::UrlWriter under the hood. And in particular, - # they use the ActionController::UrlWriter#url_for method. One can generate - # the same path as the above example by using the following code: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :only_path => true) - # # => "/users/new?message=Welcome%21" - # - # Notice the :only_path => true part. This is because UrlWriter has no - # information about the website hostname that your Rails app is serving. So if you - # want to include the hostname as well, then you must also pass the :host - # argument: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :host => 'www.example.com') # Changed this. - # # => "http://www.example.com/users/new?message=Welcome%21" - # - # By default, all controllers and views have access to a special version of url_for, - # that already knows what the current hostname is. So if you use url_for in your - # controllers or your views, then you don't need to explicitly pass the :host - # argument. - # - # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' - # in full. However, mailers don't have hostname information, and what's why you'll still - # have to specify the :host argument when generating URLs in mailers. - # - # - # == URL generation for named routes - # - # UrlWriter also allows one to access methods that have been auto-generated from - # named routes. For example, suppose that you have a 'users' resource in your - # routes.rb: - # - # map.resources :users - # - # This generates, among other things, the method users_path. By default, - # this method is accessible from your controllers, views and mailers. If you need - # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlWriter in your class: - # - # class User < ActiveRecord::Base - # include ActionController::UrlWriter - # - # def base_uri - # user_path(self) - # end - # end - # - # User.find(1).base_uri # => "/users/1" - module UrlWriter - def self.included(base) #:nodoc: - ActionController::Routing::Routes.install_helpers(base) - base.mattr_accessor :default_url_options - - # The default options for urls written by this writer. Typically a :host pair is provided. - base.default_url_options ||= {} - end - - # Generate a url based on the options provided, default_url_options and the - # routes defined in routes.rb. The following options are supported: - # - # * :only_path - If true, the relative url is returned. Defaults to +false+. - # * :protocol - The protocol to connect to. Defaults to 'http'. - # * :host - Specifies the host the link should be targeted at. - # If :only_path is false, this option must be - # provided either explicitly, or via +default_url_options+. - # * :port - Optionally specify the port to connect to. - # * :anchor - An anchor name to be appended to the path. - # * :skip_relative_url_root - If true, the url is not constructed using the - # +relative_url_root+ set in ActionController::Base.relative_url_root. - # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" - # - # Any other key (:controller, :action, etc.) given to - # +url_for+ is forwarded to the Routes module. - # - # Examples: - # - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' - # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' - def url_for(options) - options = self.class.default_url_options.merge(options) - - url = '' - - unless options.delete(:only_path) - url << (options.delete(:protocol) || 'http') - url << '://' unless url.match("://") - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - - url << options.delete(:host) - url << ":#{options.delete(:port)}" if options.key?(:port) - else - # Delete the unused options to prevent their appearance in the query string. - [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } - end - trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) - url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] - generated = Routing::Routes.generate(options, {}) - url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) - url << anchor if anchor - - url - end - end - - # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. - class UrlRewriter #:nodoc: - RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] - def initialize(request, parameters) - @request, @parameters = request, parameters - end - - def rewrite(options = {}) - rewrite_url(options) - end - - def to_str - "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" - end - - alias_method :to_s, :to_str - - private - # Given a path and options, returns a rewritten URL string - def rewrite_url(options) - rewritten_url = "" - - unless options[:only_path] - rewritten_url << (options[:protocol] || @request.protocol) - rewritten_url << "://" unless rewritten_url.match("://") - rewritten_url << rewrite_authentication(options) - rewritten_url << (options[:host] || @request.host_with_port) - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) - end - - path = rewrite_path(options) - rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor] - - rewritten_url - end - - # Given a Hash of options, generates a route - def rewrite_path(options) - options = options.symbolize_keys - options.update(options[:params].symbolize_keys) if options[:params] - - if (overwrite = options.delete(:overwrite_params)) - options.update(@parameters.symbolize_keys) - options.update(overwrite.symbolize_keys) - end - - RESERVED_OPTIONS.each { |k| options.delete(k) } - - # Generates the query string, too - Routing::Routes.generate(options, @request.symbolized_path_parameters) - end - - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" - else - "" - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb deleted file mode 100644 index 714cf97861..0000000000 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ /dev/null @@ -1,130 +0,0 @@ -module ActionController - module Routing - # Much of the slow performance from routes comes from the - # complexity of expiry, :requirements matching, defaults providing - # and figuring out which url pattern to use. With named routes - # we can avoid the expense of finding the right route. So if - # they've provided the right number of arguments, and have no - # :requirements, we can just build up a string and return it. - # - # To support building optimisations for other common cases, the - # generation code is separated into several classes - module Optimisation - def generate_optimisation_block(route, kind) - return "" unless route.optimise? - OPTIMISERS.inject("") do |memo, klazz| - memo << klazz.new(route, kind).source_code - memo - end - end - - class Optimiser - attr_reader :route, :kind - GLOBAL_GUARD_CONDITIONS = [ - "(!defined?(default_url_options) || default_url_options.blank?)", - "(!defined?(controller.default_url_options) || controller.default_url_options.blank?)", - "defined?(request)", - "request" - ] - - def initialize(route, kind) - @route = route - @kind = kind - end - - def guard_conditions - ["false"] - end - - def generation_code - 'nil' - end - - def source_code - if applicable? - guard_condition = (GLOBAL_GUARD_CONDITIONS + guard_conditions).join(" && ") - "return #{generation_code} if #{guard_condition}\n" - else - "\n" - end - end - - # Temporarily disabled :url optimisation pending proper solution to - # Issues around request.host etc. - def applicable? - true - end - end - - # Given a route - # - # map.person '/people/:id' - # - # If the user calls person_url(@person), we can simply - # return a string like "/people/#{@person.to_param}" - # rather than triggering the expensive logic in +url_for+. - class PositionalArguments < Optimiser - def guard_conditions - number_of_arguments = route.required_segment_keys.size - # if they're using foo_url(:id=>2) it's one - # argument, but we don't want to generate /foos/id2 - if number_of_arguments == 1 - ["args.size == 1", "!args.first.is_a?(Hash)"] - else - ["args.size == #{number_of_arguments}"] - end - end - - def generation_code - elements = [] - idx = 0 - - if kind == :url - elements << '#{request.protocol}' - elements << '#{request.host_with_port}' - end - - elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}' - - # The last entry in route.segments appears to *always* be a - # 'divider segment' for '/' but we have assertions to ensure that - # we don't include the trailing slashes, so skip them. - (route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment| - if segment.is_a?(DynamicSegment) - elements << segment.interpolation_chunk("args[#{idx}].to_param") - idx += 1 - else - elements << segment.interpolation_chunk - end - end - %("#{elements * ''}") - end - end - - # This case is mostly the same as the positional arguments case - # above, but it supports additional query parameters as the last - # argument - class PositionalArgumentsWithAdditionalParams < PositionalArguments - def guard_conditions - ["args.size == #{route.segment_keys.size + 1}"] + - UrlRewriter::RESERVED_OPTIONS.collect{ |key| "!args.last.has_key?(:#{key})" } - end - - # This case uses almost the same code as positional arguments, - # but add a question mark and args.last.to_query on the end, - # unless the last arg is empty - def generation_code - super.insert(-2, '#{\'?\' + args.last.to_query unless args.last.empty?}') - end - - # To avoid generating "http://localhost/?host=foo.example.com" we - # can't use this optimisation on routes without any segments - def applicable? - super && route.segment_keys.size > 0 - end - end - - OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams] - end - end -end diff --git a/actionpack/lib/action_controller/routing/polymorphic_routes.rb b/actionpack/lib/action_controller/routing/polymorphic_routes.rb new file mode 100644 index 0000000000..2adf3575a7 --- /dev/null +++ b/actionpack/lib/action_controller/routing/polymorphic_routes.rb @@ -0,0 +1,210 @@ +module ActionController + # Polymorphic URL helpers are methods for smart resolution to a named route call when + # given an Active Record model instance. They are to be used in combination with + # ActionController::Resources. + # + # These methods are useful when you want to generate correct URL or path to a RESTful + # resource without having to know the exact type of the record in question. + # + # Nested resources and/or namespaces are also supported, as illustrated in the example: + # + # polymorphic_url([:admin, @article, @comment]) + # + # results in: + # + # admin_article_comment_url(@article, @comment) + # + # == Usage within the framework + # + # Polymorphic URL helpers are used in a number of places throughout the Rails framework: + # + # * url_for, so you can use it with a record as the argument, e.g. + # url_for(@article); + # * ActionView::Helpers::FormHelper uses polymorphic_path, so you can write + # form_for(@article) without having to specify :url parameter for the form + # action; + # * redirect_to (which, in fact, uses url_for) so you can write + # redirect_to(post) in your controllers; + # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs + # for feed entries. + # + # == Prefixed polymorphic helpers + # + # In addition to polymorphic_url and polymorphic_path methods, a + # number of prefixed helpers are available as a shorthand to :action => "..." + # in options. Those are: + # + # * edit_polymorphic_url, edit_polymorphic_path + # * new_polymorphic_url, new_polymorphic_path + # + # Example usage: + # + # edit_polymorphic_path(@post) # => "/posts/1/edit" + # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" + module PolymorphicRoutes + # Constructs a call to a named RESTful route for the given record and returns the + # resulting URL string. For example: + # + # # calls post_url(post) + # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" + # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" + # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" + # polymorphic_url(Comment) # => "http://example.com/comments" + # + # ==== Options + # + # * :action - Specifies the action prefix for the named route: + # :new or :edit. Default is no prefix. + # * :routing_type - Allowed values are :path or :url. + # Default is :url. + # + # ==== Examples + # + # # an Article record + # polymorphic_url(record) # same as article_url(record) + # + # # a Comment record + # polymorphic_url(record) # same as comment_url(record) + # + # # it recognizes new records and maps to the collection + # record = Comment.new + # polymorphic_url(record) # same as comments_url() + # + # # the class of a record will also map to the collection + # polymorphic_url(Comment) # same as comments_url() + # + def polymorphic_url(record_or_hash_or_array, options = {}) + if record_or_hash_or_array.kind_of?(Array) + record_or_hash_or_array = record_or_hash_or_array.compact + record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + end + + record = extract_record(record_or_hash_or_array) + record = record.to_model if record.respond_to?(:to_model) + namespace = extract_namespace(record_or_hash_or_array) + + args = case record_or_hash_or_array + when Hash; [ record_or_hash_or_array ] + when Array; record_or_hash_or_array.dup + else [ record_or_hash_or_array ] + end + + inflection = if options[:action].to_s == "new" + args.pop + :singular + elsif (record.respond_to?(:new_record?) && record.new_record?) || + (record.respond_to?(:destroyed?) && record.destroyed?) + args.pop + :plural + elsif record.is_a?(Class) + args.pop + :plural + else + :singular + end + + args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} + + named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) + + url_options = options.except(:action, :routing_type) + unless url_options.empty? + args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options + end + + __send__(named_route, *args) + end + + # Returns the path component of a URL for the given record. It uses + # polymorphic_url with :routing_type => :path. + def polymorphic_path(record_or_hash_or_array, options = {}) + polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + end + + %w(edit new).each do |action| + module_eval <<-EOT, __FILE__, __LINE__ + def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}")) # options.merge(:action => "edit")) + end # end + # + def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) + end # end + EOT + end + + def formatted_polymorphic_url(record_or_hash, options = {}) + ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) + options[:format] = record_or_hash.pop if Array === record_or_hash + polymorphic_url(record_or_hash, options) + end + + def formatted_polymorphic_path(record_or_hash, options = {}) + ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) + options[:format] = record_or_hash.pop if record_or_hash === Array + polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) + end + + private + def action_prefix(options) + options[:action] ? "#{options[:action]}_" : '' + end + + def routing_type(options) + options[:routing_type] || :url + end + + def build_named_route_call(records, namespace, inflection, options = {}) + unless records.is_a?(Array) + record = extract_record(records) + route = '' + else + record = records.pop + route = records.inject("") do |string, parent| + if parent.is_a?(Symbol) || parent.is_a?(String) + string << "#{parent}_" + else + string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize + string << "_" + end + end + end + + if record.is_a?(Symbol) || record.is_a?(String) + route << "#{record}_" + else + route << "#{RecordIdentifier.__send__("plural_class_name", record)}" + route = route.singularize if inflection == :singular + route << "_" + end + + action_prefix(options) + namespace + route + routing_type(options).to_s + end + + def extract_record(record_or_hash_or_array) + case record_or_hash_or_array + when Array; record_or_hash_or_array.last + when Hash; record_or_hash_or_array[:id] + else record_or_hash_or_array + end + end + + # Remove the first symbols from the array and return the url prefix + # implied by those symbols. + def extract_namespace(record_or_hash_or_array) + return "" unless record_or_hash_or_array.is_a?(Array) + + namespace_keys = [] + while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) + namespace_keys << record_or_hash_or_array.shift + end + + namespace_keys.map {|k| "#{k}_"}.join + end + end +end diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb deleted file mode 100644 index 9bfebff0c0..0000000000 --- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb +++ /dev/null @@ -1,167 +0,0 @@ -module ActionController - module Routing - # BEFORE: 0.191446860631307 ms/url - # AFTER: 0.029847304022858 ms/url - # Speed up: 6.4 times - # - # Route recognition is slow due to one-by-one iterating over - # a whole routeset (each map.resources generates at least 14 routes) - # and matching weird regexps on each step. - # - # We optimize this by skipping all URI segments that 100% sure can't - # be matched, moving deeper in a tree of routes (where node == segment) - # until first possible match is accured. In such case, we start walking - # a flat list of routes, matching them with accurate matcher. - # So, first step: search a segment tree for the first relevant index. - # Second step: iterate routes starting with that index. - # - # How tree is walked? We can do a recursive tests, but it's smarter: - # We just create a tree of if-s and elsif-s matching segments. - # - # We have segments of 3 flavors: - # 1) nil (no segment, route finished) - # 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg") - # 3) const (like "/posts", "/comments") - # 4) dynamic ("/:id", "file.:size.:extension") - # - # We split incoming string into segments and iterate over them. - # When segment is nil, we drop immediately, on a current node index. - # When segment is equal to some const, we step into branch. - # If none constants matched, we step into 'dynamic' branch (it's a last). - # If we can't match anything, we drop to last index on a level. - # - # Note: we maintain the original routes order, so we finish building - # steps on a first dynamic segment. - # - # - # Example. Given the routes: - # 0 /posts/ - # 1 /posts/:id - # 2 /posts/:id/comments - # 3 /posts/blah - # 4 /users/ - # 5 /users/:id - # 6 /users/:id/profile - # - # request_uri = /users/123 - # - # There will be only 4 iterations: - # 1) segm test for /posts prefix, skip all /posts/* routes - # 2) segm test for /users/ - # 3) segm test for /users/:id - # (jump to list index = 5) - # 4) full test for /users/:id => here we are! - class RouteSet - def recognize_path(path, environment={}) - result = recognize_optimized(path, environment) and return result - - # Route was not recognized. Try to find out why (maybe wrong verb). - allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } } - - if environment[:method] && !HTTP_METHODS.include?(environment[:method]) - raise NotImplemented.new(*allows) - elsif !allows.empty? - raise MethodNotAllowed.new(*allows) - else - raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}" - end - end - - def segment_tree(routes) - tree = [0] - - i = -1 - routes.each do |route| - i += 1 - # not fast, but runs only once - segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s }) - - node = tree - segments.each do |seg| - seg = :dynamic if seg && seg[0] == ?: - node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg - node = node[node.size - 1][1] - end - end - tree - end - - def generate_code(list, padding=' ', level = 0) - # a digit - return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0]) - - body = padding + "(seg = segments[#{level}]; \n" - - i = 0 - was_nil = false - list.each do |item| - if Array === item - i += 1 - start = (i == 1) - tag, sub = item - if tag == :dynamic - body += padding + "#{start ? 'if' : 'elsif'} true\n" - body += generate_code(sub, padding + " ", level + 1) - break - elsif tag == nil && !was_nil - was_nil = true - body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n" - body += generate_code(sub, padding + " ", level + 1) - else - body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n" - body += generate_code(sub, padding + " ", level + 1) - end - end - end - body += padding + "else\n" - body += padding + " #{list[0]}\n" - body += padding + "end)\n" - body - end - - # this must be really fast - def to_plain_segments(str) - str = str.dup - str.sub!(/^\/+/,'') - str.sub!(/\/+$/,'') - segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also - segments << nil - segments - end - - private - def write_recognize_optimized! - tree = segment_tree(routes) - body = generate_code(tree) - - remove_recognize_optimized! - - instance_eval %{ - def recognize_optimized(path, env) - segments = to_plain_segments(path) - index = #{body} - return nil unless index - while index < routes.size - result = routes[index].recognize(path, env) and return result - index += 1 - end - nil - end - }, '(recognize_optimized)', 1 - end - - def clear_recognize_optimized! - remove_recognize_optimized! - write_recognize_optimized! - end - - def remove_recognize_optimized! - if respond_to?(:recognize_optimized) - class << self - remove_method :recognize_optimized - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb deleted file mode 100644 index eba05a3c5a..0000000000 --- a/actionpack/lib/action_controller/routing/route.rb +++ /dev/null @@ -1,267 +0,0 @@ -require 'active_support/core_ext/object/misc' - -module ActionController - module Routing - class Route #:nodoc: - attr_accessor :segments, :requirements, :conditions, :optimise - - def initialize(segments = [], requirements = {}, conditions = {}) - @segments = segments - @requirements = requirements - @conditions = conditions - - if !significant_keys.include?(:action) && !requirements[:action] - @requirements[:action] = "index" - @significant_keys << :action - end - - # Routes cannot use the current string interpolation method - # if there are user-supplied :requirements as the interpolation - # code won't raise RoutingErrors when generating - has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp } - if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION - @optimise = false - else - @optimise = true - end - end - - # Indicates whether the routes should be optimised with the string interpolation - # version of the named routes methods. - def optimise? - @optimise && ActionController::Base::optimise_named_routes - end - - def segment_keys - segments.collect do |segment| - segment.key if segment.respond_to? :key - end.compact - end - - def required_segment_keys - required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) } - required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact - end - - # Build a query string from the keys of the given hash. If +only_keys+ - # is given (as an array), only the keys indicated will be used to build - # the query string. The query string will correctly build array parameter - # values. - def build_query_string(hash, only_keys = nil) - elements = [] - - (only_keys || hash.keys).each do |key| - if value = hash[key] - elements << value.to_query(key) - end - end - - elements.empty? ? '' : "?#{elements.sort * '&'}" - end - - # A route's parameter shell contains parameter values that are not in the - # route's path, but should be placed in the recognized hash. - # - # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: - # - # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ - # - def parameter_shell - @parameter_shell ||= {}.tap do |shell| - requirements.each do |key, requirement| - shell[key] = requirement unless requirement.is_a? Regexp - end - end - end - - # Return an array containing all the keys that are used in this route. This - # includes keys that appear inside the path, and keys that have requirements - # placed upon them. - def significant_keys - @significant_keys ||= [].tap do |sk| - segments.each { |segment| sk << segment.key if segment.respond_to? :key } - sk.concat requirements.keys - sk.uniq! - end - end - - # Return a hash of key/value pairs representing the keys in the route that - # have defaults, or which are specified by non-regexp requirements. - def defaults - @defaults ||= {}.tap do |hash| - segments.each do |segment| - next unless segment.respond_to? :default - hash[segment.key] = segment.default unless segment.default.nil? - end - requirements.each do |key,req| - next if Regexp === req || req.nil? - hash[key] = req - end - end - end - - def matches_controller_and_action?(controller, action) - prepare_matching! - (@controller_requirement.nil? || @controller_requirement === controller) && - (@action_requirement.nil? || @action_requirement === action) - end - - def to_s - @to_s ||= begin - segs = segments.inject("") { |str,s| str << s.to_s } - "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] - end - end - - # TODO: Route should be prepared and frozen on initialize - def freeze - unless frozen? - write_generation! - write_recognition! - prepare_matching! - - parameter_shell - significant_keys - defaults - to_s - end - - super - end - - def generate(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - append_query_string(path, hash, extra_keys(options)) - end - - def generate_extras(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - [path, extra_keys(options)] - end - - private - def requirement_for(key) - return requirements[key] if requirements.key? key - segments.each do |segment| - return segment.regexp if segment.respond_to?(:key) && segment.key == key - end - nil - end - - # Write and compile a +generate+ method for this Route. - def write_generation! - # Build the main body of the generation - body = "expired = false\n#{generation_extraction}\n#{generation_structure}" - - # If we have conditions that must be tested first, nest the body inside an if - body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements - args = "options, hash, expire_on = {}" - - # Nest the body inside of a def block, and then compile it. - raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - - # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash - # are the same as the keys that were recalled from the previous request. Thus, - # we can use the expire_on.keys to determine which keys ought to be used to build - # the query string. (Never use keys from the recalled request when building the - # query string.) - - raw_method - end - - # Build several lines of code that extract values from the options hash. If any - # of the values are missing or rejected then a return will be executed. - def generation_extraction - segments.collect do |segment| - segment.extraction_code - end.compact * "\n" - end - - # Produce a condition expression that will check the requirements of this route - # upon generation. - def generation_requirements - requirement_conditions = requirements.collect do |key, req| - if req.is_a? Regexp - value_regexp = Regexp.new "\\A#{req.to_s}\\Z" - "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" - else - "hash[:#{key}] == #{req.inspect}" - end - end - requirement_conditions * ' && ' unless requirement_conditions.empty? - end - - def generation_structure - segments.last.string_structure segments[0..-2] - end - - # Write and compile a +recognize+ method for this Route. - def write_recognition! - # Create an if structure to extract the params from a match if it occurs. - body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" - body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" - - # Build the method declaration and compile it - method_decl = "def recognize(path, env = {})\n#{body}\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - method_decl - end - - # Plugins may override this method to add other conditions, like checks on - # host, subdomain, and so forth. Note that changes here only affect route - # recognition, not generation. - def recognition_conditions - result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] - result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method] - result - end - - # Build the regular expression pattern that will match this route. - def recognition_pattern(wrap = true) - pattern = '' - segments.reverse_each do |segment| - pattern = segment.build_pattern pattern - end - wrap ? ("\\A" + pattern + "\\Z") : pattern - end - - # Write the code to extract the parameters from a matched route. - def recognition_extraction - next_capture = 1 - extraction = segments.collect do |segment| - x = segment.match_extraction(next_capture) - next_capture += segment.number_of_captures - x - end - extraction.compact - end - - # Generate the query string with any extra keys in the hash and append - # it to the given path, returning the new path. - def append_query_string(path, hash, query_keys = nil) - return nil unless path - query_keys ||= extra_keys(hash) - "#{path}#{build_query_string(hash, query_keys)}" - end - - # Determine which keys in the given hash are "extra". Extra keys are - # those that were not used to generate a particular route. The extra - # keys also do not include those recalled from the prior request, nor - # do they include any keys that were implied in the route (like a - # :controller that is required, but not explicitly used in the - # text of the route.) - def extra_keys(hash, recall = {}) - (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys - end - - def prepare_matching! - unless defined? @matching_prepared - @controller_requirement = requirement_for(:controller) - @action_requirement = requirement_for(:action) - @matching_prepared = true - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 25fdbf480e..8135b5811e 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -1,6 +1,58 @@ +require 'rack/mount' +require 'forwardable' + module ActionController module Routing class RouteSet #:nodoc: + NotFound = lambda { |env| + raise RoutingError, "No route matches #{env[::Rack::Mount::Const::PATH_INFO].inspect} with #{env.inspect}" + } + + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + + class Dispatcher + def initialize(options = {}) + defaults = options[:defaults] + @glob_param = options.delete(:glob) + end + + def call(env) + params = env[PARAMETERS_KEY] + merge_default_action!(params) + split_glob_param!(params) if @glob_param + params.each { |key, value| params[key] = URI.unescape(value) if value.is_a?(String) } + + if env['action_controller.recognize'] + [200, {}, params] + else + controller = controller(params) + controller.action(params[:action]).call(env) + end + end + + private + def controller(params) + if params && params.has_key?(:controller) + controller = "#{params[:controller].camelize}Controller" + ActiveSupport::Inflector.constantize(controller) + end + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end + end + + module RouteExtensions + def segment_keys + conditions[:path_info].names.compact.map { |key| key.to_sym } + end + end + # Mapper instances are used to build routes. The object passed to the draw # block in config/routes.rb is a Mapper instance. # @@ -63,7 +115,6 @@ module ActionController # named routes. class NamedRouteCollection #:nodoc: include Enumerable - include ActionController::Routing::Optimisation attr_reader :routes, :helpers def initialize @@ -175,8 +226,6 @@ module ActionController named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks def #{selector}(*args) # def users_url(*args) # - #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)} - # opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first args.first || {} # args.first || {} else # else @@ -216,28 +265,18 @@ module ActionController clear! end - # Subclasses and plugins may override this method to specify a different - # RouteBuilder instance, so that other route DSL's can be created. - def builder - @builder ||= RouteBuilder.new - end - def draw clear! yield Mapper.new(self) + @set.add_route(NotFound) install_helpers + @set.freeze end def clear! routes.clear named_routes.clear - - @combined_regexp = nil - @routes_by_controller = nil - - # This will force routing/recognition_optimization.rb - # to refresh optimisations. - clear_recognize_optimized! + @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) end def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) @@ -257,7 +296,7 @@ module ActionController def configuration_file=(path) add_configuration_file(path) end - + # Deprecated accessor def configuration_file configuration_files @@ -296,29 +335,119 @@ module ActionController def routes_changed_at routes_changed_at = nil - + configuration_files.each do |config| config_changed_at = File.stat(config).mtime if routes_changed_at.nil? || config_changed_at > routes_changed_at - routes_changed_at = config_changed_at + routes_changed_at = config_changed_at end end - + routes_changed_at end def add_route(path, options = {}) - options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) } - route = builder.build(path, options) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) + + if glob && !defaults[glob].blank? + raise RoutingError, "paths cannot have non-empty default values" + end + end + + app = Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + route = @set.add_route(app, conditions, defaults, name) + route.extend(RouteExtensions) routes << route route end def add_named_route(name, path, options = {}) - # TODO - is options EVER used? - name = options[:name_prefix] + name.to_s if options[:name_prefix] - named_routes[name.to_sym] = add_route(path, options) + options[:_name] = name + route = add_route(path, options) + named_routes[route.name] = route + route end def options_as_params(options) @@ -356,24 +485,29 @@ module ActionController generate(options, recall, :generate_extras) end - def generate(options, recall = {}, method=:generate) - named_route_name = options.delete(:use_route) - generate_all = options.delete(:generate_all) - if named_route_name - named_route = named_routes[named_route_name] - options = named_route.parameter_shell.merge(options) - end + def generate(options, recall = {}, method = :generate) + options, recall = options.dup, recall.dup + named_route = options.delete(:use_route) options = options_as_params(options) expire_on = build_expiry(options, recall) - if options[:controller] - options[:controller] = options[:controller].to_s + recall[:action] ||= 'index' if options[:controller] || recall[:controller] + + if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) + options[:controller] = recall.delete(:controller) + + if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) + options[:action] = recall.delete(:action) + + if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) + options[:id] = recall.delete(:id) + end + end end - # if the controller has changed, make sure it changes relative to the - # current controller module, if any. In other words, if we're currently - # on admin/get, and the new controller is 'set', the new controller - # should really be admin/set. + + options[:controller] = options[:controller].to_s if options[:controller] + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ old_parts = recall[:controller].split('/') new_parts = options[:controller].split('/') @@ -381,98 +515,75 @@ module ActionController options[:controller] = parts.join('/') end - # drop the leading '/' on the controller name options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ - merged = recall.merge(options) - if named_route - path = named_route.generate(options, merged, expire_on) - if path.nil? - raise_named_route_error(options, named_route, named_route_name) - else - return path - end + merged = options.merge(recall) + if options.has_key?(:action) && options[:action].nil? + options.delete(:action) + recall[:action] = 'index' + end + recall[:action] = options.delete(:action) if options[:action] == 'index' + + path = _uri(named_route, options, recall) + if path && method == :generate_extras + uri = URI(path) + extras = uri.query ? + Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : + [] + [uri.path, extras] + elsif path + path else - merged[:action] ||= 'index' - options[:action] ||= 'index' - - controller = merged[:controller] - action = merged[:action] - - raise RoutingError, "Need controller and action!" unless controller && action - - if generate_all - # Used by caching to expire all paths for a resource - return routes.collect do |route| - route.__send__(method, options, merged, expire_on) - end.compact - end - - # don't use the recalled keys when determining which routes to check - routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }] - - routes.each_with_index do |route, index| - results = route.__send__(method, options, merged, expire_on) - if results && (!results.is_a?(Array) || results.first) - return results - end - end + raise RoutingError, "No route matches #{options.inspect}" end - + rescue Rack::Mount::RoutingError raise RoutingError, "No route matches #{options.inspect}" end - # try to give a helpful error message when named route generation fails - def raise_named_route_error(options, named_route, named_route_name) - diff = named_route.requirements.diff(options) - unless diff.empty? - raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" + def call(env) + @set.call(env) + rescue ActionController::RoutingError => e + raise e if env['action_controller.rescue_error'] == false + + method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] + + # Route was not recognized. Try to find out why (maybe wrong verb). + allows = HTTP_METHODS.select { |verb| + begin + recognize_path(path, {:method => verb}, false) + rescue ActionController::RoutingError + nil + end + } + + if !HTTP_METHODS.include?(method) + raise NotImplemented.new(*allows) + elsif !allows.empty? + raise MethodNotAllowed.new(*allows) else - required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } - required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment - raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" + raise e end end - def call(env) - request = ActionDispatch::Request.new(env) - app = Routing::Routes.recognize(request) - app.action(request.parameters[:action] || 'index').call(env) - end - def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access "#{params[:controller].to_s.camelize}Controller".constantize end - def recognize_path(path, environment={}) - raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." - end + def recognize_path(path, environment = {}, rescue_error = true) + method = (environment[:method] || "GET").to_s.upcase - def routes_by_controller - @routes_by_controller ||= Hash.new do |controller_hash, controller| - controller_hash[controller] = Hash.new do |action_hash, action| - action_hash[action] = Hash.new do |key_hash, keys| - key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) - end - end + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise RoutingError, e.message end - end - - def routes_for(options, merged, expire_on) - raise "Need controller and action!" unless controller && action - controller = merged[:controller] - merged = options if expire_on[:controller] - action = merged[:action] || 'index' - - routes_by_controller[controller][action][merged.keys][1] - end - def routes_for_controller_and_action_and_keys(controller, action, keys) - routes.select do |route| - route.matches_controller_and_action? controller, action - end + env['action_controller.recognize'] = true + env['action_controller.rescue_error'] = rescue_error + status, headers, body = call(env) + body end # Subclasses and plugins may override this method to extract further attributes @@ -480,6 +591,109 @@ module ActionController def extract_request_environment(request) { :method => request.method } end + + private + def _uri(named_route, params, recall) + params = URISegment.wrap_values(params) + recall = URISegment.wrap_values(recall) + + unless result = @set.generate(:path_info, named_route, params, recall) + return + end + + uri, params = result + params.each do |k, v| + if v._value + params[k] = v._value + else + params.delete(k) + end + end + + uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? + uri + end + + class URISegment < Struct.new(:_value, :_escape) + EXCLUDED = [:controller] + + def self.wrap_values(hash) + hash.inject({}) { |h, (k, v)| + h[k] = new(v, !EXCLUDED.include?(k.to_sym)) + h + } + end + + extend Forwardable + def_delegators :_value, :==, :eql?, :hash + + def to_param + @to_param ||= begin + if _value.is_a?(Array) + _value.map { |v| _escaped(v) }.join('/') + else + _escaped(_value) + end + end + end + alias_method :to_s, :to_param + + private + def _escaped(value) + v = value.respond_to?(:to_param) ? value.to_param : value + _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s + end + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end end end end diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb deleted file mode 100644 index 5e5b22b6c2..0000000000 --- a/actionpack/lib/action_controller/routing/routing_ext.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_support/core_ext/object/conversions' -require 'active_support/core_ext/boolean/conversions' -require 'active_support/core_ext/nil/conversions' -require 'active_support/core_ext/regexp' diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb deleted file mode 100644 index 2603855476..0000000000 --- a/actionpack/lib/action_controller/routing/segments.rb +++ /dev/null @@ -1,343 +0,0 @@ -module ActionController - module Routing - class Segment #:nodoc: - RESERVED_PCHAR = ':@&=+$,;%' - SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" - if RUBY_VERSION >= '1.9' - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze - else - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze - end - - # TODO: Convert :is_optional accessor to read only - attr_accessor :is_optional - alias_method :optional?, :is_optional - - def initialize - @is_optional = false - end - - def number_of_captures - Regexp.new(regexp_chunk).number_of_captures - end - - def extraction_code - nil - end - - # Continue generating string for the prior segments. - def continue_string_structure(prior_segments) - if prior_segments.empty? - interpolation_statement(prior_segments) - else - new_priors = prior_segments[0..-2] - prior_segments.last.string_structure(new_priors) - end - end - - def interpolation_chunk - URI.escape(value, UNSAFE_PCHAR) - end - - # Return a string interpolation statement for this segment and those before it. - def interpolation_statement(prior_segments) - chunks = prior_segments.collect { |s| s.interpolation_chunk } - chunks << interpolation_chunk - "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" - end - - def string_structure(prior_segments) - optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) - end - - # Return an if condition that is true if all the prior segments can be generated. - # If there are no optional segments before this one, then nil is returned. - def all_optionals_available_condition(prior_segments) - optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact - optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" - end - - # Recognition - - def match_extraction(next_capture) - nil - end - - # Warning - - # Returns true if this segment is optional? because of a default. If so, then - # no warning will be emitted regarding this segment. - def optionality_implied? - false - end - end - - class StaticSegment < Segment #:nodoc: - attr_reader :value, :raw - alias_method :raw?, :raw - - def initialize(value = nil, options = {}) - super() - @value = value - @raw = options[:raw] if options.key?(:raw) - @is_optional = options[:optional] if options.key?(:optional) - end - - def interpolation_chunk - raw? ? value : super - end - - def regexp_chunk - chunk = Regexp.escape(value) - optional? ? Regexp.optionalize(chunk) : chunk - end - - def number_of_captures - 0 - end - - def build_pattern(pattern) - escaped = Regexp.escape(value) - if optional? && ! pattern.empty? - "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" - elsif optional? - Regexp.optionalize escaped - else - escaped + pattern - end - end - - def to_s - value - end - end - - class DividerSegment < StaticSegment #:nodoc: - def initialize(value = nil, options = {}) - super(value, {:raw => true, :optional => true}.merge(options)) - end - - def optionality_implied? - true - end - end - - class DynamicSegment < Segment #:nodoc: - attr_reader :key - - # TODO: Convert these accessors to read only - attr_accessor :default, :regexp - - def initialize(key = nil, options = {}) - super() - @key = key - @default = options[:default] if options.key?(:default) - @regexp = options[:regexp] if options.key?(:regexp) - @is_optional = true if options[:optional] || options.key?(:default) - end - - def to_s - ":#{key}" - end - - # The local variable name that the value of this segment will be extracted to. - def local_name - "#{key}_value" - end - - def extract_value - "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" - end - - def value_check - if default # Then we know it won't be nil - "#{value_regexp.inspect} =~ #{local_name}" if regexp - elsif optional? - # If we have a regexp check that the value is not given, or that it matches. - # If we have no regexp, return nil since we do not require a condition. - "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp - else # Then it must be present, and if we have a regexp, it must match too. - "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" - end - end - - def expiry_statement - "expired, hash = true, options if !expired && expire_on[:#{key}]" - end - - def extraction_code - s = extract_value - vc = value_check - s << "\nreturn [nil,nil] unless #{vc}" if vc - s << "\n#{expiry_statement}" - end - - def interpolation_chunk(value_code = local_name) - "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" - end - - def string_structure(prior_segments) - if optional? # We have a conditional to do... - # If we should not appear in the url, just write the code for the prior - # segments. This occurs if our value is the default value, or, if we are - # optional, if we have nil as our value. - "if #{local_name} == #{default.inspect}\n" + - continue_string_structure(prior_segments) + - "\nelse\n" + # Otherwise, write the code up to here - "#{interpolation_statement(prior_segments)}\nend" - else - interpolation_statement(prior_segments) - end - end - - def value_regexp - Regexp.new "\\A#{regexp.to_s}\\Z" if regexp - end - - def regexp_chunk - regexp ? regexp_string : default_regexp_chunk - end - - def regexp_string - regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})" - end - - def default_regexp_chunk - "([^#{Routing::SEPARATORS.join}]+)" - end - - def number_of_captures - regexp ? regexp.number_of_captures + 1 : 1 - end - - def build_pattern(pattern) - pattern = "#{regexp_chunk}#{pattern}" - optional? ? Regexp.optionalize(pattern) : pattern - end - - def match_extraction(next_capture) - # All non code-related keys (such as :id, :slug) are URI-unescaped as - # path parameters. - default_value = default ? default.inspect : nil - %[ - value = if (m = match[#{next_capture}]) - URI.unescape(m) - else - #{default_value} - end - params[:#{key}] = value if value - ] - end - - def optionality_implied? - [:action, :id].include? key - end - - def regexp_has_modifiers? - regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 - end - end - - class ControllerSegment < DynamicSegment #:nodoc: - def regexp_chunk - possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } - "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" - end - - # Don't URI.escape the controller name since it may contain slashes. - def interpolation_chunk(value_code = local_name) - "\#{#{value_code}.to_s}" - end - - # Make sure controller names like Admin/Content are correctly normalized to - # admin/content - def extract_value - "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" - end - - def match_extraction(next_capture) - if default - "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'" - else - "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]" - end - end - end - - class PathSegment < DynamicSegment #:nodoc: - def interpolation_chunk(value_code = local_name) - "\#{#{value_code}}" - end - - def extract_value - "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" - end - - def default - '' - end - - def default=(path) - raise RoutingError, "paths cannot have non-empty default values" unless path.blank? - end - - def match_extraction(next_capture) - "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" - end - - def default_regexp_chunk - "(.*)" - end - - def number_of_captures - regexp ? regexp.number_of_captures : 1 - end - - def optionality_implied? - true - end - - class Result < ::Array #:nodoc: - def to_s() join '/' end - def self.new_escaped(strings) - new strings.collect {|str| URI.unescape str} - end - end - end - - # The OptionalFormatSegment allows for any resource route to have an optional - # :format, which decreases the amount of routes created by 50%. - class OptionalFormatSegment < DynamicSegment - - def initialize(key = nil, options = {}) - super(:format, {:optional => true}.merge(options)) - end - - def interpolation_chunk - "." + super - end - - def regexp_chunk - '/|(\.[^/?\.]+)?' - end - - def to_s - '(.:format)?' - end - - def extract_value - "#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase" - end - - #the value should not include the period (.) - def match_extraction(next_capture) - %[ - if (m = match[#{next_capture}]) - params[:#{key}] = URI.unescape(m.from(1)) - end - ] - end - end - - end -end diff --git a/actionpack/lib/action_controller/routing/url_rewriter.rb b/actionpack/lib/action_controller/routing/url_rewriter.rb new file mode 100644 index 0000000000..52b66c9303 --- /dev/null +++ b/actionpack/lib/action_controller/routing/url_rewriter.rb @@ -0,0 +1,204 @@ +module ActionController + # In routes.rb one defines URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. + # + # See ActionController::Routing and ActionController::Resources for general + # information about routing and routes.rb. + # + # Tip: If you need to generate URLs from your models or some other place, + # then ActionController::UrlWriter is what you're looking for. Read on for + # an introduction. + # + # == URL generation from parameters + # + # As you may know, some functions - such as ActionController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', :controller => 'users', + # :action => 'new', :message => 'Welcome!') %> + # + # #=> Generates a link to: /users/new?message=Welcome%21 + # + # link_to, and all other functions that require URL generation functionality, + # actually use ActionController::UrlWriter under the hood. And in particular, + # they use the ActionController::UrlWriter#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :only_path => true) + # # => "/users/new?message=Welcome%21" + # + # Notice the :only_path => true part. This is because UrlWriter has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the :host + # argument: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :host => 'www.example.com') # Changed this. + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the :host + # argument. + # + # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. + # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' + # in full. However, mailers don't have hostname information, and what's why you'll still + # have to specify the :host argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlWriter also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # routes.rb: + # + # map.resources :users + # + # This generates, among other things, the method users_path. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that by including ActionController::UrlWriter in your class: + # + # class User < ActiveRecord::Base + # include ActionController::UrlWriter + # + # def base_uri + # user_path(self) + # end + # end + # + # User.find(1).base_uri # => "/users/1" + module UrlWriter + def self.included(base) #:nodoc: + ActionController::Routing::Routes.install_helpers(base) + base.mattr_accessor :default_url_options + + # The default options for urls written by this writer. Typically a :host pair is provided. + base.default_url_options ||= {} + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * :only_path - If true, the relative url is returned. Defaults to +false+. + # * :protocol - The protocol to connect to. Defaults to 'http'. + # * :host - Specifies the host the link should be targeted at. + # If :only_path is false, this option must be + # provided either explicitly, or via +default_url_options+. + # * :port - Optionally specify the port to connect to. + # * :anchor - An anchor name to be appended to the path. + # * :skip_relative_url_root - If true, the url is not constructed using the + # +relative_url_root+ set in ActionController::Base.relative_url_root. + # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" + # + # Any other key (:controller, :action, etc.) given to + # +url_for+ is forwarded to the Routes module. + # + # Examples: + # + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + def url_for(options) + options = self.class.default_url_options.merge(options) + + url = '' + + unless options.delete(:only_path) + url << (options.delete(:protocol) || 'http') + url << '://' unless url.match("://") + + raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] + + url << options.delete(:host) + url << ":#{options.delete(:port)}" if options.key?(:port) + else + # Delete the unused options to prevent their appearance in the query string. + [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } + end + trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) + url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] + anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] + generated = Routing::Routes.generate(options, {}) + url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) + url << anchor if anchor + + url + end + end + + # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. + class UrlRewriter #:nodoc: + RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] + def initialize(request, parameters) + @request, @parameters = request, parameters + end + + def rewrite(options = {}) + rewrite_url(options) + end + + def to_str + "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" + end + + alias_method :to_s, :to_str + + private + # Given a path and options, returns a rewritten URL string + def rewrite_url(options) + rewritten_url = "" + + unless options[:only_path] + rewritten_url << (options[:protocol] || @request.protocol) + rewritten_url << "://" unless rewritten_url.match("://") + rewritten_url << rewrite_authentication(options) + rewritten_url << (options[:host] || @request.host_with_port) + rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) + end + + path = rewrite_path(options) + rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor] + + rewritten_url + end + + # Given a Hash of options, generates a route + def rewrite_path(options) + options = options.symbolize_keys + options.update(options[:params].symbolize_keys) if options[:params] + + if (overwrite = options.delete(:overwrite_params)) + options.update(@parameters.symbolize_keys) + options.update(overwrite.symbolize_keys) + end + + RESERVED_OPTIONS.each { |k| options.delete(k) } + + # Generates the query string, too + Routing::Routes.generate(options, @request.symbolized_path_parameters) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" + else + "" + end + end + end +end diff --git a/actionpack/lib/action_controller/testing/test_case.rb b/actionpack/lib/action_controller/testing/test_case.rb index 178e3477a6..01a55fe930 100644 --- a/actionpack/lib/action_controller/testing/test_case.rb +++ b/actionpack/lib/action_controller/testing/test_case.rb @@ -10,6 +10,13 @@ module ActionController self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16)) end + class Result < ::Array #:nodoc: + def to_s() join '/' end + def self.new_escaped(strings) + new strings.collect {|str| URI.unescape str} + end + end + def assign_parameters(controller_path, action, parameters = {}) parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) extra_keys = ActionController::Routing::Routes.extra_keys(parameters) @@ -18,7 +25,7 @@ module ActionController if value.is_a? Fixnum value = value.to_s elsif value.is_a? Array - value = ActionController::Routing::PathSegment::Result.new(value) + value = Result.new(value) end if extra_keys.include?(key.to_sym) diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 67448e66b9..cbfc8267f2 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -11,14 +11,6 @@ end ROUTING = ActionController::Routing -class ROUTING::RouteBuilder - attr_reader :warn_output - - def warn(msg) - (@warn_output ||= []) << msg - end -end - # See RFC 3986, section 3.3 for allowed path characters. class UriReservedCharactersRoutingTest < Test::Unit::TestCase def setup @@ -1626,25 +1618,6 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal '/pages/show/hello+world', default_route_set.generate(expected, expected) end - def test_parameter_shell - page_url = ROUTING::Route.new - page_url.requirements = {:controller => 'pages', :action => 'show', :id => /\d+/} - assert_equal({:controller => 'pages', :action => 'show'}, page_url.parameter_shell) - end - - def test_defaults - route = ROUTING::RouteBuilder.new.build '/users/:id.:format', :controller => "users", :action => "show", :format => "html" - assert_equal( - { :controller => "users", :action => "show", :format => "html" }, - route.defaults) - end - - def test_builder_complains_without_controller - assert_raise(ArgumentError) do - ROUTING::RouteBuilder.new.build '/contact', :contoller => "contact", :action => "index" - end - end - def test_build_empty_query_string assert_uri_equal '/foo', default_route_set.generate({:controller => 'foo'}) end -- cgit v1.2.3 From 3895e2ccb3fe7cfcf36794fe9c941dc1381a0cb7 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 20 Oct 2009 09:52:59 -0500 Subject: Donate tests I wrote for rackmount rails integration --- actionpack/test/controller/routing_test.rb | 314 ++++++++++++++++++++- actionpack/test/lib/controller/fake_controllers.rb | 5 +- 2 files changed, 316 insertions(+), 3 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index cbfc8267f2..308e2a85b1 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -110,8 +110,6 @@ class LegacyRouteSetTests < Test::Unit::TestCase ActionController::Base.optimise_named_routes = true @rs = ::ActionController::Routing::RouteSet.new - - ActionController::Routing.use_controllers! %w(content admin/user admin/news_feed) end def teardown @@ -1736,3 +1734,315 @@ class RouteLoadingTest < Test::Unit::TestCase ActionController::Routing::Routes end end + +class RackMountIntegrationTests < ActiveSupport::TestCase + Model = Struct.new(:to_param) + + Mapping = lambda { |map| + map.namespace :admin do |admin| + admin.resources :users + end + + map.namespace 'api' do |api| + api.root :controller => 'users' + end + + map.connect 'blog/:year/:month/:day', + :controller => 'posts', + :action => 'show_date', + :requirements => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/}, + :day => nil, + :month => nil + + map.blog('archive/:year', :controller => 'archive', :action => 'index', + :defaults => { :year => nil }, + :requirements => { :year => /\d{4}/ } + ) + + map.resources :people + map.connect 'legacy/people', :controller => 'people', :action => 'index', :legacy => 'true' + + map.connect 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol + map.connect 'id_default/:id', :controller => 'foo', :action => 'id_default', :id => 1 + map.connect 'get_or_post', :controller => 'foo', :action => 'get_or_post', :conditions => { :method => [:get, :post] } + map.connect 'optional/:optional', :controller => 'posts', :action => 'index' + map.project 'projects/:project_id', :controller => 'project' + map.connect 'clients', :controller => 'projects', :action => 'index' + + map.connect 'ignorecase/geocode/:postalcode', :controller => 'geocode', + :action => 'show', :postalcode => /hx\d\d-\d[a-z]{2}/i + map.geocode 'extended/geocode/:postalcode', :controller => 'geocode', + :action => 'show',:requirements => { + :postalcode => /# Postcode format + \d{5} #Prefix + (-\d{4})? #Suffix + /x + } + + map.connect '', :controller => 'news', :format => nil + map.connect 'news.:format', :controller => 'news' + + map.connect 'comment/:id/:action', :controller => 'comments', :action => 'show' + map.connect 'ws/:controller/:action/:id', :ws => true + map.connect 'account/:action', :controller => :account, :action => :subscription + map.connect 'pages/:page_id/:controller/:action/:id' + map.connect ':controller/ping', :action => 'ping' + map.connect ':controller/:action/:id' + } + + def setup + @routes = ActionController::Routing::RouteSet.new + @routes.draw(&Mapping) + end + + def test_add_route + @routes.clear! + + assert_raise(ActionController::RoutingError) do + @routes.draw do |map| + map.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) + map.connect ':controller/:action/:id' + end + end + end + + def test_recognize_path + assert_equal({:controller => 'admin/users', :action => 'index'}, @routes.recognize_path('/admin/users', :method => :get)) + assert_equal({:controller => 'admin/users', :action => 'create'}, @routes.recognize_path('/admin/users', :method => :post)) + assert_equal({:controller => 'admin/users', :action => 'new'}, @routes.recognize_path('/admin/users/new', :method => :get)) + assert_equal({:controller => 'admin/users', :action => 'show', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :get)) + assert_equal({:controller => 'admin/users', :action => 'update', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :put)) + assert_equal({:controller => 'admin/users', :action => 'destroy', :id => '1'}, @routes.recognize_path('/admin/users/1', :method => :delete)) + assert_equal({:controller => 'admin/users', :action => 'edit', :id => '1'}, @routes.recognize_path('/admin/users/1/edit', :method => :get)) + + assert_equal({:controller => 'admin/posts', :action => 'index'}, @routes.recognize_path('/admin/posts', :method => :get)) + assert_equal({:controller => 'admin/posts', :action => 'new'}, @routes.recognize_path('/admin/posts/new', :method => :get)) + + assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api', :method => :get)) + assert_equal({:controller => 'api/users', :action => 'index'}, @routes.recognize_path('/api/', :method => :get)) + + assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009'}, @routes.recognize_path('/blog/2009', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01'}, @routes.recognize_path('/blog/2009/01', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show_date', :year => '2009', :month => '01', :day => '01'}, @routes.recognize_path('/blog/2009/01/01', :method => :get)) + assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/blog/123456789', :method => :get) } + + assert_equal({:controller => 'archive', :action => 'index', :year => '2010'}, @routes.recognize_path('/archive/2010')) + assert_equal({:controller => 'archive', :action => 'index'}, @routes.recognize_path('/archive')) + assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/archive/january') } + + assert_equal({:controller => 'people', :action => 'index'}, @routes.recognize_path('/people', :method => :get)) + assert_equal({:controller => 'people', :action => 'index', :format => 'xml'}, @routes.recognize_path('/people.xml', :method => :get)) + assert_equal({:controller => 'people', :action => 'create'}, @routes.recognize_path('/people', :method => :post)) + assert_equal({:controller => 'people', :action => 'new'}, @routes.recognize_path('/people/new', :method => :get)) + assert_equal({:controller => 'people', :action => 'show', :id => '1'}, @routes.recognize_path('/people/1', :method => :get)) + assert_equal({:controller => 'people', :action => 'show', :id => '1', :format => 'xml'}, @routes.recognize_path('/people/1.xml', :method => :get)) + assert_equal({:controller => 'people', :action => 'update', :id => '1'}, @routes.recognize_path('/people/1', :method => :put)) + assert_equal({:controller => 'people', :action => 'destroy', :id => '1'}, @routes.recognize_path('/people/1', :method => :delete)) + assert_equal({:controller => 'people', :action => 'edit', :id => '1'}, @routes.recognize_path('/people/1/edit', :method => :get)) + assert_equal({:controller => 'people', :action => 'edit', :id => '1', :format => 'xml'}, @routes.recognize_path('/people/1/edit.xml', :method => :get)) + + assert_equal({:controller => 'symbols', :action => 'show', :name => :as_symbol}, @routes.recognize_path('/symbols')) + assert_equal({:controller => 'foo', :action => 'id_default', :id => '1'}, @routes.recognize_path('/id_default/1')) + assert_equal({:controller => 'foo', :action => 'id_default', :id => '2'}, @routes.recognize_path('/id_default/2')) + assert_equal({:controller => 'foo', :action => 'id_default', :id => '1'}, @routes.recognize_path('/id_default')) + assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get)) + assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post)) + assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) } + assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) } + + assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar')) + assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') } + + assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get)) + assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get)) + assert_equal({:controller => 'posts', :action => 'index', :ws => true}, @routes.recognize_path('/ws/posts', :method => :get)) + + assert_equal({:controller => 'account', :action => 'subscription'}, @routes.recognize_path('/account', :method => :get)) + assert_equal({:controller => 'account', :action => 'subscription'}, @routes.recognize_path('/account/subscription', :method => :get)) + assert_equal({:controller => 'account', :action => 'billing'}, @routes.recognize_path('/account/billing', :method => :get)) + + assert_equal({:page_id => '1', :controller => 'notes', :action => 'index'}, @routes.recognize_path('/pages/1/notes', :method => :get)) + assert_equal({:page_id => '1', :controller => 'notes', :action => 'list'}, @routes.recognize_path('/pages/1/notes/list', :method => :get)) + assert_equal({:page_id => '1', :controller => 'notes', :action => 'show', :id => '2'}, @routes.recognize_path('/pages/1/notes/show/2', :method => :get)) + + assert_equal({:controller => 'posts', :action => 'ping'}, @routes.recognize_path('/posts/ping', :method => :get)) + assert_equal({:controller => 'posts', :action => 'index'}, @routes.recognize_path('/posts', :method => :get)) + assert_equal({:controller => 'posts', :action => 'index'}, @routes.recognize_path('/posts/index', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show'}, @routes.recognize_path('/posts/show', :method => :get)) + assert_equal({:controller => 'posts', :action => 'show', :id => '1'}, @routes.recognize_path('/posts/show/1', :method => :get)) + assert_equal({:controller => 'posts', :action => 'create'}, @routes.recognize_path('/posts/create', :method => :post)) + + assert_equal({:controller => 'geocode', :action => 'show', :postalcode => 'hx12-1az'}, @routes.recognize_path('/ignorecase/geocode/hx12-1az')) + assert_equal({:controller => 'geocode', :action => 'show', :postalcode => 'hx12-1AZ'}, @routes.recognize_path('/ignorecase/geocode/hx12-1AZ')) + assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345-1234'}, @routes.recognize_path('/extended/geocode/12345-1234')) + assert_equal({:controller => 'geocode', :action => 'show', :postalcode => '12345'}, @routes.recognize_path('/extended/geocode/12345')) + + assert_equal({:controller => 'news', :action => 'index', :format => nil}, @routes.recognize_path('/', :method => :get)) + assert_equal({:controller => 'news', :action => 'index', :format => 'rss'}, @routes.recognize_path('/news.rss', :method => :get)) + + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/none', :method => :get) } + end + + def test_generate + assert_equal '/admin/users', @routes.generate(:use_route => 'admin_users') + assert_equal '/admin/users', @routes.generate(:controller => 'admin/users') + assert_equal '/admin/users', @routes.generate(:controller => 'admin/users', :action => 'index') + assert_equal '/admin/users', @routes.generate({:action => 'index'}, {:controller => 'admin/users'}) + assert_equal '/admin/users', @routes.generate({:controller => 'users', :action => 'index'}, {:controller => 'admin/accounts'}) + assert_equal '/people', @routes.generate({:controller => '/people', :action => 'index'}, {:controller => 'admin/accounts'}) + + assert_equal '/admin/posts', @routes.generate({:controller => 'admin/posts'}) + assert_equal '/admin/posts/new', @routes.generate({:controller => 'admin/posts', :action => 'new'}) + + assert_equal '/blog/2009', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009) + assert_equal '/blog/2009/1', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009, :month => 1) + assert_equal '/blog/2009/1/1', @routes.generate(:controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1) + + assert_equal '/archive/2010', @routes.generate(:controller => 'archive', :action => 'index', :year => '2010') + assert_equal '/archive', @routes.generate(:controller => 'archive', :action => 'index') + assert_equal '/archive?year=january', @routes.generate(:controller => 'archive', :action => 'index', :year => 'january') + + assert_equal '/people', @routes.generate(:use_route => 'people') + assert_equal '/people', @routes.generate(:use_route => 'people', :controller => 'people', :action => 'index') + assert_equal '/people.xml', @routes.generate(:use_route => 'people', :controller => 'people', :action => 'index', :format => 'xml') + assert_equal '/people', @routes.generate({:use_route => 'people', :controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'index'}) + assert_equal '/people', @routes.generate(:controller => 'people') + assert_equal '/people', @routes.generate(:controller => 'people', :action => 'index') + assert_equal '/people', @routes.generate({:action => 'index'}, {:controller => 'people'}) + assert_equal '/people', @routes.generate({:action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) + assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) + assert_equal '/people', @routes.generate({}, {:controller => 'people', :action => 'index'}) + assert_equal '/people/1', @routes.generate({:controller => 'people', :action => 'show'}, {:controller => 'people', :action => 'show', :id => '1'}) + assert_equal '/people/new', @routes.generate(:use_route => 'new_person') + assert_equal '/people/new', @routes.generate(:controller => 'people', :action => 'new') + assert_equal '/people/1', @routes.generate(:use_route => 'person', :id => '1') + assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => '1') + assert_equal '/people/1.xml', @routes.generate(:controller => 'people', :action => 'show', :id => '1', :format => 'xml') + assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => 1) + assert_equal '/people/1', @routes.generate(:controller => 'people', :action => 'show', :id => Model.new('1')) + assert_equal '/people/1', @routes.generate({:action => 'show', :id => '1'}, {:controller => 'people', :action => 'index'}) + assert_equal '/people/1', @routes.generate({:action => 'show', :id => 1}, {:controller => 'people', :action => 'show', :id => '1'}) + # assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'index', :id => '1'}) + assert_equal '/people', @routes.generate({:controller => 'people', :action => 'index'}, {:controller => 'people', :action => 'show', :id => '1'}) + assert_equal '/people/1', @routes.generate({}, {:controller => 'people', :action => 'show', :id => '1'}) + assert_equal '/people/1', @routes.generate({:controller => 'people', :action => 'show'}, {:controller => 'people', :action => 'index', :id => '1'}) + assert_equal '/people/1/edit', @routes.generate(:controller => 'people', :action => 'edit', :id => '1') + assert_equal '/people/1/edit.xml', @routes.generate(:controller => 'people', :action => 'edit', :id => '1', :format => 'xml') + assert_equal '/people/1/edit', @routes.generate(:use_route => 'edit_person', :id => '1') + assert_equal '/people/1?legacy=true', @routes.generate(:controller => 'people', :action => 'show', :id => '1', :legacy => 'true') + assert_equal '/people?legacy=true', @routes.generate(:controller => 'people', :action => 'index', :legacy => 'true') + + assert_equal '/id_default/2', @routes.generate(:controller => 'foo', :action => 'id_default', :id => '2') + assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default', :id => '1') + assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default', :id => 1) + assert_equal '/id_default', @routes.generate(:controller => 'foo', :action => 'id_default') + assert_equal '/optional/bar', @routes.generate(:controller => 'posts', :action => 'index', :optional => 'bar') + assert_equal '/posts', @routes.generate(:controller => 'posts', :action => 'index') + + assert_equal '/project', @routes.generate({:controller => 'project', :action => 'index'}) + assert_equal '/projects/1', @routes.generate({:controller => 'project', :action => 'index', :project_id => '1'}) + assert_equal '/projects/1', @routes.generate({:controller => 'project', :action => 'index'}, {:project_id => '1'}) + assert_raise(ActionController::RoutingError) { @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index'}) } + assert_equal '/projects/1', @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1'}) + assert_equal '/projects/1', @routes.generate({:use_route => 'project', :controller => 'project', :action => 'index'}, {:project_id => '1'}) + + assert_equal '/clients', @routes.generate(:controller => 'projects', :action => 'index') + assert_equal '/clients?project_id=1', @routes.generate(:controller => 'projects', :action => 'index', :project_id => '1') + assert_equal '/clients', @routes.generate({:controller => 'projects', :action => 'index'}, {:project_id => '1'}) + assert_equal '/clients', @routes.generate({:action => 'index'}, {:controller => 'projects', :action => 'index', :project_id => '1'}) + + assert_equal '/comment/20', @routes.generate({:id => 20}, {:controller => 'comments', :action => 'show'}) + assert_equal '/comment/20', @routes.generate(:controller => 'comments', :id => 20, :action => 'show') + assert_equal '/comments/boo', @routes.generate(:controller => 'comments', :action => 'boo') + + assert_equal '/ws/posts/show/1', @routes.generate(:controller => 'posts', :action => 'show', :id => '1', :ws => true) + assert_equal '/ws/posts', @routes.generate(:controller => 'posts', :action => 'index', :ws => true) + + assert_equal '/account', @routes.generate(:controller => 'account', :action => 'subscription') + assert_equal '/account/billing', @routes.generate(:controller => 'account', :action => 'billing') + + assert_equal '/pages/1/notes/show/1', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'show', :id => '1') + assert_equal '/pages/1/notes/list', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'list') + assert_equal '/pages/1/notes', @routes.generate(:page_id => '1', :controller => 'notes', :action => 'index') + assert_equal '/pages/1/notes', @routes.generate(:page_id => '1', :controller => 'notes') + assert_equal '/notes', @routes.generate(:page_id => nil, :controller => 'notes') + assert_equal '/notes', @routes.generate(:controller => 'notes') + assert_equal '/notes/print', @routes.generate(:controller => 'notes', :action => 'print') + assert_equal '/notes/print', @routes.generate({}, {:controller => 'notes', :action => 'print'}) + + assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1'}) + assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1', :foo => 'bar'}) + assert_equal '/notes/index/1', @routes.generate({:controller => 'notes'}, {:controller => 'notes', :id => '1'}) + assert_equal '/notes/index/1', @routes.generate({:action => 'index'}, {:controller => 'notes', :id => '1'}) + assert_equal '/notes/index/1', @routes.generate({}, {:controller => 'notes', :id => '1'}) + assert_equal '/notes/show/1', @routes.generate({}, {:controller => 'notes', :action => 'show', :id => '1'}) + assert_equal '/notes/index/1', @routes.generate({:controller => 'notes', :id => '1'}, {:foo => 'bar'}) + assert_equal '/posts', @routes.generate({:controller => 'posts'}, {:controller => 'notes', :action => 'show', :id => '1'}) + assert_equal '/notes/list', @routes.generate({:action => 'list'}, {:controller => 'notes', :action => 'show', :id => '1'}) + + assert_equal '/posts/ping', @routes.generate(:controller => 'posts', :action => 'ping') + assert_equal '/posts/show/1', @routes.generate(:controller => 'posts', :action => 'show', :id => '1') + assert_equal '/posts', @routes.generate(:controller => 'posts') + assert_equal '/posts', @routes.generate(:controller => 'posts', :action => 'index') + assert_equal '/posts', @routes.generate({:controller => 'posts'}, {:controller => 'posts', :action => 'index'}) + assert_equal '/posts/create', @routes.generate({:action => 'create'}, {:controller => 'posts'}) + assert_equal '/posts?foo=bar', @routes.generate(:controller => 'posts', :foo => 'bar') + assert_equal '/posts?foo%5B%5D=bar&foo%5B%5D=baz', @routes.generate(:controller => 'posts', :foo => ['bar', 'baz']) + assert_equal '/posts?page=2', @routes.generate(:controller => 'posts', :page => 2) + assert_equal '/posts?q%5Bfoo%5D%5Ba%5D=b', @routes.generate(:controller => 'posts', :q => { :foo => { :a => 'b'}}) + + assert_equal '/', @routes.generate(:controller => 'news', :action => 'index') + assert_equal '/', @routes.generate(:controller => 'news', :action => 'index', :format => nil) + assert_equal '/news.rss', @routes.generate(:controller => 'news', :action => 'index', :format => 'rss') + + + assert_raise(ActionController::RoutingError) { @routes.generate({:action => 'index'}) } + end + + def test_generate_extras + assert_equal ['/people', []], @routes.generate_extras(:controller => 'people') + assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :foo => 'bar') + assert_equal ['/people', []], @routes.generate_extras(:controller => 'people', :action => 'index') + assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :action => 'index', :foo => 'bar') + assert_equal ['/people/new', []], @routes.generate_extras(:controller => 'people', :action => 'new') + assert_equal ['/people/new', [:foo]], @routes.generate_extras(:controller => 'people', :action => 'new', :foo => 'bar') + assert_equal ['/people/1', []], @routes.generate_extras(:controller => 'people', :action => 'show', :id => '1') + assert_equal ['/people/1', [:bar, :foo]], sort_extras!(@routes.generate_extras(:controller => 'people', :action => 'show', :id => '1', :foo => '2', :bar => '3')) + assert_equal ['/people', [:person]], @routes.generate_extras(:controller => 'people', :action => 'create', :person => { :first_name => 'Josh', :last_name => 'Peek' }) + assert_equal ['/people', [:people]], @routes.generate_extras(:controller => 'people', :action => 'create', :people => ['Josh', 'Dave']) + + assert_equal ['/posts/show/1', []], @routes.generate_extras(:controller => 'posts', :action => 'show', :id => '1') + assert_equal ['/posts/show/1', [:bar, :foo]], sort_extras!(@routes.generate_extras(:controller => 'posts', :action => 'show', :id => '1', :foo => '2', :bar => '3')) + assert_equal ['/posts', []], @routes.generate_extras(:controller => 'posts', :action => 'index') + assert_equal ['/posts', [:foo]], @routes.generate_extras(:controller => 'posts', :action => 'index', :foo => 'bar') + end + + def test_extras + params = {:controller => 'people'} + assert_equal [], @routes.extra_keys(params) + assert_equal({:controller => 'people'}, params) + + params = {:controller => 'people', :foo => 'bar'} + assert_equal [:foo], @routes.extra_keys(params) + assert_equal({:controller => 'people', :foo => 'bar'}, params) + + params = {:controller => 'people', :action => 'create', :person => { :name => 'Josh'}} + assert_equal [:person], @routes.extra_keys(params) + assert_equal({:controller => 'people', :action => 'create', :person => { :name => 'Josh'}}, params) + end + + private + def sort_extras!(extras) + if extras.length == 2 + extras[1].sort! { |a, b| a.to_s <=> b.to_s } + end + extras + end + + def assert_raise(e) + result = yield + flunk "Did not raise #{e}, but returned #{result.inspect}" + rescue e + assert true + end +end diff --git a/actionpack/test/lib/controller/fake_controllers.rb b/actionpack/test/lib/controller/fake_controllers.rb index c993836a61..250327e6dc 100644 --- a/actionpack/test/lib/controller/fake_controllers.rb +++ b/actionpack/test/lib/controller/fake_controllers.rb @@ -5,9 +5,10 @@ class NotAController; end module Admin class << self; alias_method :const_available?, :const_defined?; end - class UserController < ActionController::Base; end class NewsFeedController < ActionController::Base; end + class PostsController < ActionController::Base; end class StuffController < ActionController::Base; end + class UserController < ActionController::Base; end end module Api @@ -25,7 +26,9 @@ class ElsewhereController < ActionController::Base; end class FooController < ActionController::Base; end class HiController < ActionController::Base; end class ImageController < ActionController::Base; end +class NotesController < ActionController::Base; end class PeopleController < ActionController::Base; end +class PostsController < ActionController::Base; end class SessionsController < ActionController::Base; end class StuffController < ActionController::Base; end class SubpathBooksController < ActionController::Base; end -- cgit v1.2.3 From a74022ecd3e078f55ed6049a96565119dc540ff5 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 20 Oct 2009 10:14:46 -0500 Subject: Move Routing into AD --- actionpack/lib/action_controller.rb | 13 +- actionpack/lib/action_controller/deprecated.rb | 3 + .../lib/action_controller/polymorphic_routes.rb | 210 +++++++ actionpack/lib/action_controller/routing.rb | 381 ----------- .../routing/polymorphic_routes.rb | 210 ------- .../lib/action_controller/routing/resources.rb | 685 -------------------- .../lib/action_controller/routing/route_set.rb | 699 --------------------- .../lib/action_controller/routing/url_rewriter.rb | 204 ------ actionpack/lib/action_controller/url_rewriter.rb | 204 ++++++ actionpack/lib/action_dispatch.rb | 2 + actionpack/lib/action_dispatch/routing.rb | 381 +++++++++++ .../lib/action_dispatch/routing/resources.rb | 687 ++++++++++++++++++++ .../lib/action_dispatch/routing/route_set.rb | 699 +++++++++++++++++++++ actionpack/test/controller/resources_test.rb | 12 +- 14 files changed, 2197 insertions(+), 2193 deletions(-) create mode 100644 actionpack/lib/action_controller/polymorphic_routes.rb delete mode 100644 actionpack/lib/action_controller/routing.rb delete mode 100644 actionpack/lib/action_controller/routing/polymorphic_routes.rb delete mode 100644 actionpack/lib/action_controller/routing/resources.rb delete mode 100644 actionpack/lib/action_controller/routing/route_set.rb delete mode 100644 actionpack/lib/action_controller/routing/url_rewriter.rb create mode 100644 actionpack/lib/action_controller/url_rewriter.rb create mode 100644 actionpack/lib/action_dispatch/routing.rb create mode 100644 actionpack/lib/action_dispatch/routing/resources.rb create mode 100644 actionpack/lib/action_dispatch/routing/route_set.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 809e5f2ad4..b5091f39f9 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -18,22 +18,21 @@ module ActionController autoload :Testing, "action_controller/metal/testing" autoload :UrlFor, "action_controller/metal/url_for" - # Ported modules - # require 'action_controller/routing' autoload :Caching, 'action_controller/caching' autoload :Dispatcher, 'action_controller/dispatch/dispatcher' autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :MimeResponds, 'action_controller/metal/mime_responds' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :PolymorphicRoutes, 'action_controller/routing/polymorphic_routes' + autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' - autoload :Resources, 'action_controller/routing/resources' + autoload :Resources, 'action_controller/deprecated' + autoload :Routing, 'action_controller/deprecated' autoload :SessionManagement, 'action_controller/metal/session_management' autoload :TestCase, 'action_controller/testing/test_case' autoload :TestProcess, 'action_controller/testing/process' - autoload :UrlRewriter, 'action_controller/routing/url_rewriter' - autoload :UrlWriter, 'action_controller/routing/url_rewriter' + autoload :UrlRewriter, 'action_controller/url_rewriter' + autoload :UrlWriter, 'action_controller/url_rewriter' autoload :Verification, 'action_controller/metal/verification' autoload :Flash, 'action_controller/metal/flash' @@ -54,8 +53,6 @@ module ActionController autoload :RenderError, 'action_controller/metal/exceptions' autoload :SessionOverflowError, 'action_controller/metal/exceptions' autoload :UnknownHttpMethod, 'action_controller/metal/exceptions' - - autoload :Routing, 'action_controller/routing' end autoload :HTML, 'action_controller/vendor/html-scanner' diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index d98e9ac7bd..23fe6a4c3a 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,2 +1,5 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response +ActionController::Routing = ActionDispatch::Routing +ActionDispatch::Resources = ActionDispatch::Routing::Resources +ActionController::Routing::Routes = ActionDispatch::Routing::RouteSet.new diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb new file mode 100644 index 0000000000..2adf3575a7 --- /dev/null +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -0,0 +1,210 @@ +module ActionController + # Polymorphic URL helpers are methods for smart resolution to a named route call when + # given an Active Record model instance. They are to be used in combination with + # ActionController::Resources. + # + # These methods are useful when you want to generate correct URL or path to a RESTful + # resource without having to know the exact type of the record in question. + # + # Nested resources and/or namespaces are also supported, as illustrated in the example: + # + # polymorphic_url([:admin, @article, @comment]) + # + # results in: + # + # admin_article_comment_url(@article, @comment) + # + # == Usage within the framework + # + # Polymorphic URL helpers are used in a number of places throughout the Rails framework: + # + # * url_for, so you can use it with a record as the argument, e.g. + # url_for(@article); + # * ActionView::Helpers::FormHelper uses polymorphic_path, so you can write + # form_for(@article) without having to specify :url parameter for the form + # action; + # * redirect_to (which, in fact, uses url_for) so you can write + # redirect_to(post) in your controllers; + # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs + # for feed entries. + # + # == Prefixed polymorphic helpers + # + # In addition to polymorphic_url and polymorphic_path methods, a + # number of prefixed helpers are available as a shorthand to :action => "..." + # in options. Those are: + # + # * edit_polymorphic_url, edit_polymorphic_path + # * new_polymorphic_url, new_polymorphic_path + # + # Example usage: + # + # edit_polymorphic_path(@post) # => "/posts/1/edit" + # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" + module PolymorphicRoutes + # Constructs a call to a named RESTful route for the given record and returns the + # resulting URL string. For example: + # + # # calls post_url(post) + # polymorphic_url(post) # => "http://example.com/posts/1" + # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" + # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" + # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" + # polymorphic_url(Comment) # => "http://example.com/comments" + # + # ==== Options + # + # * :action - Specifies the action prefix for the named route: + # :new or :edit. Default is no prefix. + # * :routing_type - Allowed values are :path or :url. + # Default is :url. + # + # ==== Examples + # + # # an Article record + # polymorphic_url(record) # same as article_url(record) + # + # # a Comment record + # polymorphic_url(record) # same as comment_url(record) + # + # # it recognizes new records and maps to the collection + # record = Comment.new + # polymorphic_url(record) # same as comments_url() + # + # # the class of a record will also map to the collection + # polymorphic_url(Comment) # same as comments_url() + # + def polymorphic_url(record_or_hash_or_array, options = {}) + if record_or_hash_or_array.kind_of?(Array) + record_or_hash_or_array = record_or_hash_or_array.compact + record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + end + + record = extract_record(record_or_hash_or_array) + record = record.to_model if record.respond_to?(:to_model) + namespace = extract_namespace(record_or_hash_or_array) + + args = case record_or_hash_or_array + when Hash; [ record_or_hash_or_array ] + when Array; record_or_hash_or_array.dup + else [ record_or_hash_or_array ] + end + + inflection = if options[:action].to_s == "new" + args.pop + :singular + elsif (record.respond_to?(:new_record?) && record.new_record?) || + (record.respond_to?(:destroyed?) && record.destroyed?) + args.pop + :plural + elsif record.is_a?(Class) + args.pop + :plural + else + :singular + end + + args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} + + named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) + + url_options = options.except(:action, :routing_type) + unless url_options.empty? + args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options + end + + __send__(named_route, *args) + end + + # Returns the path component of a URL for the given record. It uses + # polymorphic_url with :routing_type => :path. + def polymorphic_path(record_or_hash_or_array, options = {}) + polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + end + + %w(edit new).each do |action| + module_eval <<-EOT, __FILE__, __LINE__ + def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}")) # options.merge(:action => "edit")) + end # end + # + def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) + end # end + EOT + end + + def formatted_polymorphic_url(record_or_hash, options = {}) + ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) + options[:format] = record_or_hash.pop if Array === record_or_hash + polymorphic_url(record_or_hash, options) + end + + def formatted_polymorphic_path(record_or_hash, options = {}) + ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) + options[:format] = record_or_hash.pop if record_or_hash === Array + polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) + end + + private + def action_prefix(options) + options[:action] ? "#{options[:action]}_" : '' + end + + def routing_type(options) + options[:routing_type] || :url + end + + def build_named_route_call(records, namespace, inflection, options = {}) + unless records.is_a?(Array) + record = extract_record(records) + route = '' + else + record = records.pop + route = records.inject("") do |string, parent| + if parent.is_a?(Symbol) || parent.is_a?(String) + string << "#{parent}_" + else + string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize + string << "_" + end + end + end + + if record.is_a?(Symbol) || record.is_a?(String) + route << "#{record}_" + else + route << "#{RecordIdentifier.__send__("plural_class_name", record)}" + route = route.singularize if inflection == :singular + route << "_" + end + + action_prefix(options) + namespace + route + routing_type(options).to_s + end + + def extract_record(record_or_hash_or_array) + case record_or_hash_or_array + when Array; record_or_hash_or_array.last + when Hash; record_or_hash_or_array[:id] + else record_or_hash_or_array + end + end + + # Remove the first symbols from the array and return the url prefix + # implied by those symbols. + def extract_namespace(record_or_hash_or_array) + return "" unless record_or_hash_or_array.is_a?(Array) + + namespace_keys = [] + while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) + namespace_keys << record_or_hash_or_array.shift + end + + namespace_keys.map {|k| "#{k}_"}.join + end + end +end diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb deleted file mode 100644 index 979a4ad8c9..0000000000 --- a/actionpack/lib/action_controller/routing.rb +++ /dev/null @@ -1,381 +0,0 @@ -require 'active_support/core_ext/object/conversions' -require 'active_support/core_ext/boolean/conversions' -require 'active_support/core_ext/nil/conversions' -require 'active_support/core_ext/regexp' -require 'action_controller/routing/route_set' - -module ActionController - # == Routing - # - # The routing module provides URL rewriting in native Ruby. It's a way to - # redirect incoming requests to controllers and actions. This replaces - # mod_rewrite rules. Best of all, Rails' Routing works with any web server. - # Routes are defined in config/routes.rb. - # - # Consider the following route, installed by Rails when you generate your - # application: - # - # map.connect ':controller/:action/:id' - # - # This route states that it expects requests to consist of a - # :controller followed by an :action that in turn is fed - # some :id. - # - # Suppose you get an incoming request for /blog/edit/22, you'll end up - # with: - # - # params = { :controller => 'blog', - # :action => 'edit', - # :id => '22' - # } - # - # Think of creating routes as drawing a map for your requests. The map tells - # them where to go based on some predefined pattern: - # - # ActionController::Routing::Routes.draw do |map| - # Pattern 1 tells some request to go to one place - # Pattern 2 tell them to go to another - # ... - # end - # - # The following symbols are special: - # - # :controller maps to your controller name - # :action maps to an action with your controllers - # - # Other names simply map to a parameter as in the case of :id. - # - # == Route priority - # - # Not all routes are created equally. Routes have priority defined by the - # order of appearance of the routes in the config/routes.rb file. The priority goes - # from top to bottom. The last route in that file is at the lowest priority - # and will be applied last. If no route matches, 404 is returned. - # - # Within blocks, the empty pattern is at the highest priority. - # In practice this works out nicely: - # - # ActionController::Routing::Routes.draw do |map| - # map.with_options :controller => 'blog' do |blog| - # blog.show '', :action => 'list' - # end - # map.connect ':controller/:action/:view' - # end - # - # In this case, invoking blog controller (with an URL like '/blog/') - # without parameters will activate the 'list' action by default. - # - # == Defaults routes and default parameters - # - # Setting a default route is straightforward in Rails - you simply append a - # Hash at the end of your mapping to set any default parameters. - # - # Example: - # - # ActionController::Routing:Routes.draw do |map| - # map.connect ':controller/:action/:id', :controller => 'blog' - # end - # - # This sets up +blog+ as the default controller if no other is specified. - # This means visiting '/' would invoke the blog controller. - # - # More formally, you can include arbitrary parameters in the route, thus: - # - # map.connect ':controller/:action/:id', :action => 'show', :page => 'Dashboard' - # - # This will pass the :page parameter to all incoming requests that match this route. - # - # Note: The default routes, as provided by the Rails generator, make all actions in every - # controller accessible via GET requests. You should consider removing them or commenting - # them out if you're using named routes and resources. - # - # == Named routes - # - # Routes can be named with the syntax map.name_of_route options, - # allowing for easy reference within your source as +name_of_route_url+ - # for the full URL and +name_of_route_path+ for the URI path. - # - # Example: - # - # # In routes.rb - # map.login 'login', :controller => 'accounts', :action => 'login' - # - # # With render, redirect_to, tests, etc. - # redirect_to login_url - # - # Arguments can be passed as well. - # - # redirect_to show_item_path(:id => 25) - # - # Use map.root as a shorthand to name a route for the root path "". - # - # # In routes.rb - # map.root :controller => 'blogs' - # - # # would recognize http://www.example.com/ as - # params = { :controller => 'blogs', :action => 'index' } - # - # # and provide these named routes - # root_url # => 'http://www.example.com/' - # root_path # => '' - # - # You can also specify an already-defined named route in your map.root call: - # - # # In routes.rb - # map.new_session :controller => 'sessions', :action => 'new' - # map.root :new_session - # - # Note: when using +with_options+, the route is simply named after the - # method you call on the block parameter rather than map. - # - # # In routes.rb - # map.with_options :controller => 'blog' do |blog| - # blog.show '', :action => 'list' - # blog.delete 'delete/:id', :action => 'delete' - # blog.edit 'edit/:id', :action => 'edit' - # end - # - # # provides named routes for show, delete, and edit - # link_to @article.title, show_path(:id => @article.id) - # - # == Pretty URLs - # - # Routes can generate pretty URLs. For example: - # - # map.connect 'articles/:year/:month/:day', - # :controller => 'articles', - # :action => 'find_by_date', - # :year => /\d{4}/, - # :month => /\d{1,2}/, - # :day => /\d{1,2}/ - # - # Using the route above, the URL "http://localhost:3000/articles/2005/11/06" - # maps to - # - # params = {:year => '2005', :month => '11', :day => '06'} - # - # == Regular Expressions and parameters - # You can specify a regular expression to define a format for a parameter. - # - # map.geocode 'geocode/:postalcode', :controller => 'geocode', - # :action => 'show', :postalcode => /\d{5}(-\d{4})?/ - # - # or, more formally: - # - # map.geocode 'geocode/:postalcode', :controller => 'geocode', - # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ } - # - # Formats can include the 'ignorecase' and 'extended syntax' regular - # expression modifiers: - # - # map.geocode 'geocode/:postalcode', :controller => 'geocode', - # :action => 'show', :postalcode => /hx\d\d\s\d[a-z]{2}/i - # - # map.geocode 'geocode/:postalcode', :controller => 'geocode', - # :action => 'show',:requirements => { - # :postalcode => /# Postcode format - # \d{5} #Prefix - # (-\d{4})? #Suffix - # /x - # } - # - # Using the multiline match modifier will raise an ArgumentError. - # Encoding regular expression modifiers are silently ignored. The - # match will always use the default encoding or ASCII. - # - # == Route globbing - # - # Specifying *[string] as part of a rule like: - # - # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' - # - # will glob all remaining parts of the route that were not recognized earlier. - # The globbed values are in params[:path] as an array of path segments. - # - # == Route conditions - # - # With conditions you can define restrictions on routes. Currently the only valid condition is :method. - # - # * :method - Allows you to specify which method can access the route. Possible values are :post, - # :get, :put, :delete and :any. The default value is :any, - # :any means that any method can access the route. - # - # Example: - # - # map.connect 'post/:id', :controller => 'posts', :action => 'show', - # :conditions => { :method => :get } - # map.connect 'post/:id', :controller => 'posts', :action => 'create_comment', - # :conditions => { :method => :post } - # - # Now, if you POST to /posts/:id, it will route to the create_comment action. A GET on the same - # URL will route to the show action. - # - # == Reloading routes - # - # You can reload routes if you feel you must: - # - # ActionController::Routing::Routes.reload - # - # This will clear all named routes and reload routes.rb if the file has been modified from - # last load. To absolutely force reloading, use reload!. - # - # == Testing Routes - # - # The two main methods for testing your routes: - # - # === +assert_routing+ - # - # def test_movie_route_properly_splits - # opts = {:controller => "plugin", :action => "checkout", :id => "2"} - # assert_routing "plugin/checkout/2", opts - # end - # - # +assert_routing+ lets you test whether or not the route properly resolves into options. - # - # === +assert_recognizes+ - # - # def test_route_has_options - # opts = {:controller => "plugin", :action => "show", :id => "12"} - # assert_recognizes opts, "/plugins/show/12" - # end - # - # Note the subtle difference between the two: +assert_routing+ tests that - # a URL fits options while +assert_recognizes+ tests that a URL - # breaks into parameters properly. - # - # In tests you can simply pass the URL or named route to +get+ or +post+. - # - # def send_to_jail - # get '/jail' - # assert_response :success - # assert_template "jail/front" - # end - # - # def goes_to_login - # get login_url - # #... - # end - # - # == View a list of all your routes - # - # Run rake routes. - # - module Routing - SEPARATORS = %w( / . ? ) - - HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] - - ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set - - # The root paths which may contain controller files - mattr_accessor :controller_paths - self.controller_paths = [] - - # A helper module to hold URL related helpers. - module Helpers - include PolymorphicRoutes - end - - class << self - # Expects an array of controller names as the first argument. - # Executes the passed block with only the named controllers named available. - # This method is used in internal Rails testing. - def with_controllers(names) - prior_controllers = @possible_controllers - use_controllers! names - yield - ensure - use_controllers! prior_controllers - end - - # Returns an array of paths, cleaned of double-slashes and relative path references. - # * "\\\" and "//" become "\\" or "/". - # * "/foo/bar/../config" becomes "/foo/config". - # The returned array is sorted by length, descending. - def normalize_paths(paths) - # do the hokey-pokey of path normalization... - paths = paths.collect do |path| - path = path. - gsub("//", "/"). # replace double / chars with a single - gsub("\\\\", "\\"). # replace double \ chars with a single - gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it - - # eliminate .. paths where possible - re = %r{[^/\\]+[/\\]\.\.[/\\]} - path.gsub!(re, "") while path.match(re) - path - end - - # start with longest path, first - paths = paths.uniq.sort_by { |path| - path.length } - end - - # Returns the array of controller names currently available to ActionController::Routing. - def possible_controllers - unless @possible_controllers - @possible_controllers = [] - - paths = controller_paths.select { |path| File.directory?(path) && path != "." } - - seen_paths = Hash.new {|h, k| h[k] = true; false} - normalize_paths(paths).each do |load_path| - Dir["#{load_path}/**/*_controller.rb"].collect do |path| - next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] - - controller_name = path[(load_path.length + 1)..-1] - - controller_name.gsub!(/_controller\.rb\Z/, '') - @possible_controllers << controller_name - end - end - - # remove duplicates - @possible_controllers.uniq! - end - @possible_controllers - end - - # Replaces the internal list of controllers available to ActionController::Routing with the passed argument. - # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ]) - def use_controllers!(controller_names) - @possible_controllers = controller_names - end - - # Returns a controller path for a new +controller+ based on a +previous+ controller path. - # Handles 4 scenarios: - # - # * stay in the previous controller: - # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion" - # - # * stay in the previous namespace: - # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts" - # - # * forced move to the root namespace: - # controller_relative_to( "/posts", "groups/discussion" ) # => "posts" - # - # * previous namespace is root: - # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" - # - def controller_relative_to(controller, previous) - if controller.nil? then previous - elsif controller[0] == ?/ then controller[1..-1] - elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" - else controller - end - end - end - - Routes = RouteSet.new - - ActiveSupport::Inflector.module_eval do - # Ensures that routes are reloaded when Rails inflections are updated. - def inflections_with_route_reloading(&block) - returning(inflections_without_route_reloading(&block)) { - ActionController::Routing::Routes.reload! if block_given? - } - end - - alias_method_chain :inflections, :route_reloading - end - end -end diff --git a/actionpack/lib/action_controller/routing/polymorphic_routes.rb b/actionpack/lib/action_controller/routing/polymorphic_routes.rb deleted file mode 100644 index 2adf3575a7..0000000000 --- a/actionpack/lib/action_controller/routing/polymorphic_routes.rb +++ /dev/null @@ -1,210 +0,0 @@ -module ActionController - # Polymorphic URL helpers are methods for smart resolution to a named route call when - # given an Active Record model instance. They are to be used in combination with - # ActionController::Resources. - # - # These methods are useful when you want to generate correct URL or path to a RESTful - # resource without having to know the exact type of the record in question. - # - # Nested resources and/or namespaces are also supported, as illustrated in the example: - # - # polymorphic_url([:admin, @article, @comment]) - # - # results in: - # - # admin_article_comment_url(@article, @comment) - # - # == Usage within the framework - # - # Polymorphic URL helpers are used in a number of places throughout the Rails framework: - # - # * url_for, so you can use it with a record as the argument, e.g. - # url_for(@article); - # * ActionView::Helpers::FormHelper uses polymorphic_path, so you can write - # form_for(@article) without having to specify :url parameter for the form - # action; - # * redirect_to (which, in fact, uses url_for) so you can write - # redirect_to(post) in your controllers; - # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs - # for feed entries. - # - # == Prefixed polymorphic helpers - # - # In addition to polymorphic_url and polymorphic_path methods, a - # number of prefixed helpers are available as a shorthand to :action => "..." - # in options. Those are: - # - # * edit_polymorphic_url, edit_polymorphic_path - # * new_polymorphic_url, new_polymorphic_path - # - # Example usage: - # - # edit_polymorphic_path(@post) # => "/posts/1/edit" - # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" - module PolymorphicRoutes - # Constructs a call to a named RESTful route for the given record and returns the - # resulting URL string. For example: - # - # # calls post_url(post) - # polymorphic_url(post) # => "http://example.com/posts/1" - # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1" - # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1" - # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1" - # polymorphic_url(Comment) # => "http://example.com/comments" - # - # ==== Options - # - # * :action - Specifies the action prefix for the named route: - # :new or :edit. Default is no prefix. - # * :routing_type - Allowed values are :path or :url. - # Default is :url. - # - # ==== Examples - # - # # an Article record - # polymorphic_url(record) # same as article_url(record) - # - # # a Comment record - # polymorphic_url(record) # same as comment_url(record) - # - # # it recognizes new records and maps to the collection - # record = Comment.new - # polymorphic_url(record) # same as comments_url() - # - # # the class of a record will also map to the collection - # polymorphic_url(Comment) # same as comments_url() - # - def polymorphic_url(record_or_hash_or_array, options = {}) - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 - end - - record = extract_record(record_or_hash_or_array) - record = record.to_model if record.respond_to?(:to_model) - namespace = extract_namespace(record_or_hash_or_array) - - args = case record_or_hash_or_array - when Hash; [ record_or_hash_or_array ] - when Array; record_or_hash_or_array.dup - else [ record_or_hash_or_array ] - end - - inflection = if options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:new_record?) && record.new_record?) || - (record.respond_to?(:destroyed?) && record.destroyed?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end - - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - - named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) - - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - __send__(named_route, *args) - end - - # Returns the path component of a URL for the given record. It uses - # polymorphic_url with :routing_type => :path. - def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) - end - - %w(edit new).each do |action| - module_eval <<-EOT, __FILE__, __LINE__ - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end - EOT - end - - def formatted_polymorphic_url(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) - options[:format] = record_or_hash.pop if Array === record_or_hash - polymorphic_url(record_or_hash, options) - end - - def formatted_polymorphic_path(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) - options[:format] = record_or_hash.pop if record_or_hash === Array - polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) - end - - private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' - end - - def routing_type(options) - options[:routing_type] || :url - end - - def build_named_route_call(records, namespace, inflection, options = {}) - unless records.is_a?(Array) - record = extract_record(records) - route = '' - else - record = records.pop - route = records.inject("") do |string, parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - string << "#{parent}_" - else - string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize - string << "_" - end - end - end - - if record.is_a?(Symbol) || record.is_a?(String) - route << "#{record}_" - else - route << "#{RecordIdentifier.__send__("plural_class_name", record)}" - route = route.singularize if inflection == :singular - route << "_" - end - - action_prefix(options) + namespace + route + routing_type(options).to_s - end - - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end - end - - # Remove the first symbols from the array and return the url prefix - # implied by those symbols. - def extract_namespace(record_or_hash_or_array) - return "" unless record_or_hash_or_array.is_a?(Array) - - namespace_keys = [] - while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) - namespace_keys << record_or_hash_or_array.shift - end - - namespace_keys.map {|k| "#{k}_"}.join - end - end -end diff --git a/actionpack/lib/action_controller/routing/resources.rb b/actionpack/lib/action_controller/routing/resources.rb deleted file mode 100644 index 06506435a2..0000000000 --- a/actionpack/lib/action_controller/routing/resources.rb +++ /dev/null @@ -1,685 +0,0 @@ -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/try' - -module ActionController - # == Overview - # - # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, - # is something that can be pointed at and it will respond with a representation of the data requested. - # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application - # requests XML data. - # - # RESTful design is based on the assumption that there are four generic verbs that a user of an - # application can request from a \resource (the noun). - # - # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used - # denotes the type of action that should take place. - # - # === The Different Methods and their Usage - # - # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. - # * POST - Creation of \resources. - # * PUT - Editing of attributes on a \resource. - # * DELETE - Deletion of a \resource. - # - # === Examples - # - # # A GET request on the Posts resource is asking for all Posts - # GET /posts - # - # # A GET request on a single Post resource is asking for that particular Post - # GET /posts/1 - # - # # A POST request on the Posts resource is asking for a Post to be created with the supplied details - # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } - # - # # A PUT request on a single Post resource is asking for a Post to be updated - # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } - # - # # A DELETE request on a single Post resource is asking for it to be deleted - # DELETE /posts # with => { :id => 1 } - # - # By using the REST convention, users of our application can assume certain things about how the data - # is requested and how it is returned. Rails simplifies the routing part of RESTful design by - # supplying you with methods to create them in your routes.rb file. - # - # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer - module Resources - INHERITABLE_OPTIONS = :namespace, :shallow - - class Resource #:nodoc: - DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy - - attr_reader :collection_methods, :member_methods, :new_methods - attr_reader :path_prefix, :name_prefix, :path_segment - attr_reader :plural, :singular - attr_reader :options - - def initialize(entities, options) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - - arrange_actions - add_default_actions - set_allowed_actions - set_prefixes - end - - def controller - @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" - end - - def requirements(with_id = false) - @requirements ||= @options[:requirements] || {} - @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } - - with_id ? @requirements.merge(@id_requirement) : @requirements - end - - def conditions - @conditions ||= @options[:conditions] || {} - end - - def path - @path ||= "#{path_prefix}/#{path_segment}" - end - - def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= Base.resources_path_names[:new] - @new_path ||= "#{path}/#{new_action}" - end - - def shallow_path_prefix - @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix - end - - def member_path - @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" - end - - def nesting_path_prefix - @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" - end - - def shallow_name_prefix - @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix - end - - def nesting_name_prefix - "#{shallow_name_prefix}#{singular}_" - end - - def action_separator - @action_separator ||= Base.resource_action_separator - end - - def uncountable? - @singular.to_s == @plural.to_s - end - - def has_action?(action) - !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) - end - - protected - def arrange_actions - @collection_methods = arrange_actions_by_methods(options.delete(:collection)) - @member_methods = arrange_actions_by_methods(options.delete(:member)) - @new_methods = arrange_actions_by_methods(options.delete(:new)) - end - - def add_default_actions - add_default_action(member_methods, :get, :edit) - add_default_action(new_methods, :get, :new) - end - - def set_allowed_actions - only, except = @options.values_at(:only, :except) - @allowed_actions ||= {} - - if only == :all || except == :none - only = nil - except = [] - elsif only == :none || except == :all - only = [] - except = nil - end - - if only - @allowed_actions[:only] = Array(only).map {|a| a.to_sym } - elsif except - @allowed_actions[:except] = Array(except).map {|a| a.to_sym } - end - end - - def action_allowed?(action) - only, except = @allowed_actions.values_at(:only, :except) - (!only || only.include?(action)) && (!except || !except.include?(action)) - end - - def set_prefixes - @path_prefix = options.delete(:path_prefix) - @name_prefix = options.delete(:name_prefix) - end - - def arrange_actions_by_methods(actions) - (actions || {}).inject({}) do |flipped_hash, (key, value)| - (flipped_hash[value] ||= []) << key - flipped_hash - end - end - - def add_default_action(collection, method, action) - (collection[method] ||= []).unshift(action) - end - end - - class SingletonResource < Resource #:nodoc: - def initialize(entity, options) - @singular = @plural = entity - options[:controller] ||= @singular.to_s.pluralize - super - end - - alias_method :shallow_path_prefix, :path_prefix - alias_method :shallow_name_prefix, :name_prefix - alias_method :member_path, :path - alias_method :nesting_path_prefix, :path - end - - # Creates named routes for implementing verb-oriented controllers - # for a collection \resource. - # - # For example: - # - # map.resources :messages - # - # will map the following actions in the corresponding controller: - # - # class MessagesController < ActionController::Base - # # GET messages_url - # def index - # # return all messages - # end - # - # # GET new_message_url - # def new - # # return an HTML form for describing a new message - # end - # - # # POST messages_url - # def create - # # create a new message - # end - # - # # GET message_url(:id => 1) - # def show - # # find and return a specific message - # end - # - # # GET edit_message_url(:id => 1) - # def edit - # # return an HTML form for editing a specific message - # end - # - # # PUT message_url(:id => 1) - # def update - # # find and update a specific message - # end - # - # # DELETE message_url(:id => 1) - # def destroy - # # delete a specific message - # end - # end - # - # Along with the routes themselves, +resources+ generates named routes for use in - # controllers and views. map.resources :messages produces the following named routes and helpers: - # - # Named Route Helpers - # ============ ===================================================== - # messages messages_url, hash_for_messages_url, - # messages_path, hash_for_messages_path - # - # message message_url(id), hash_for_message_url(id), - # message_path(id), hash_for_message_path(id) - # - # new_message new_message_url, hash_for_new_message_url, - # new_message_path, hash_for_new_message_path - # - # edit_message edit_message_url(id), hash_for_edit_message_url(id), - # edit_message_path(id), hash_for_edit_message_path(id) - # - # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: - # - # redirect_to :controller => 'messages', :action => 'index' - # # and - # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> - # - # now become: - # - # redirect_to messages_url - # # and - # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically - # - # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your - # form tags. The form helpers make this a little easier. For an update form with a @message object: - # - # <%= form_tag message_path(@message), :method => :put %> - # - # or - # - # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> - # - # or - # - # <% form_for @message do |f| %> - # - # which takes into account whether @message is a new record or not and generates the - # path and method accordingly. - # - # The +resources+ method accepts the following options to customize the resulting routes: - # * :collection - Add named routes for other actions that operate on the collection. - # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, - # an array of any of the previous, or :any if the method does not matter. - # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. - # * :member - Same as :collection, but for actions that operate on a specific member. - # * :new - Same as :collection, but for actions that operate on the new \resource action. - # * :controller - Specify the controller name for the routes. - # * :singular - Specify the singular name used in the member routes. - # * :requirements - Set custom routing parameter requirements; this is a hash of either - # regular expressions (which must match for the route to match) or extra parameters. For example: - # - # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } - # - # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. - # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. - # * :as - Specify a different \resource name to use in the URL path. For example: - # # products_path == '/productos' - # map.resources :products, :as => 'productos' do |product| - # # product_reviews_path(product) == '/productos/1234/comentarios' - # product.resources :product_reviews, :as => 'comentarios' - # end - # - # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. - # * :has_many - Same has :has_one, but for plural \resources. - # - # You may directly specify the routing association with +has_one+ and +has_many+ like: - # - # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] - # - # This is the same as: - # - # map.resources :notes do |notes| - # notes.resource :author - # notes.resources :comments - # notes.resources :attachments - # end - # - # * :path_names - Specify different path names for the actions. For example: - # # new_products_path == '/productos/nuevo' - # # bids_product_path(1) == '/productos/1/licitacoes' - # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } - # - # You can also set default action names from an environment, like this: - # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } - # - # * :path_prefix - Set a prefix to the routes with required route variables. - # - # Weblog comments usually belong to a post, so you might use +resources+ like: - # - # map.resources :articles - # map.resources :comments, :path_prefix => '/articles/:article_id' - # - # You can nest +resources+ calls to set this automatically: - # - # map.resources :articles do |article| - # article.resources :comments - # end - # - # The comment \resources work the same, but must now include a value for :article_id. - # - # article_comments_url(@article) - # article_comment_url(@article, @comment) - # - # article_comments_url(:article_id => @article) - # article_comment_url(:article_id => @article, :id => @comment) - # - # If you don't want to load all objects from the database you might want to use the article_id directly: - # - # articles_comments_url(@comment.article_id, @comment) - # - # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. - # Use this if you have named routes that may clash. - # - # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' - # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' - # - # You may also use :name_prefix to override the generic named routes in a nested \resource: - # - # map.resources :articles do |article| - # article.resources :comments, :name_prefix => nil - # end - # - # This will yield named \resources like so: - # - # comments_url(@article) - # comment_url(@article, @comment) - # - # * :shallow - If true, paths for nested resources which reference a specific member - # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. - # - # The :shallow option is inherited by any nested resource(s). - # - # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: - # - # map.resources :users, :shallow => true do |user| - # user.resources :posts do |post| - # post.resources :comments - # end - # end - # # --> GET /users/1/posts (maps to the PostsController#index action as usual) - # # also adds the usual named route called "user_posts" - # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) - # # also adds the named route called "post" - # # --> GET /posts/2/comments (maps to the CommentsController#index action) - # # also adds the named route called "post_comments" - # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) - # # also adds the named route called "comment" - # - # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: - # - # map.resources :users, :has_many => { :posts => :comments }, :shallow => true - # - # * :only and :except - Specify which of the seven default actions should be routed to. - # - # :only and :except may be set to :all, :none, an action name or a - # list of action names. By default, routes are generated for all seven actions. - # - # For example: - # - # map.resources :posts, :only => [:index, :show] do |post| - # post.resources :comments, :except => [:update, :destroy] - # end - # # --> GET /posts (maps to the PostsController#index action) - # # --> POST /posts (fails) - # # --> GET /posts/1 (maps to the PostsController#show action) - # # --> DELETE /posts/1 (fails) - # # --> POST /posts/1/comments (maps to the CommentsController#create action) - # # --> PUT /posts/1/comments/1 (fails) - # - # If map.resources is called with multiple resources, they all get the same options applied. - # - # Examples: - # - # map.resources :messages, :path_prefix => "/thread/:thread_id" - # # --> GET /thread/7/messages/1 - # - # map.resources :messages, :collection => { :rss => :get } - # # --> GET /messages/rss (maps to the #rss action) - # # also adds a named route called "rss_messages" - # - # map.resources :messages, :member => { :mark => :post } - # # --> POST /messages/1/mark (maps to the #mark action) - # # also adds a named route called "mark_message" - # - # map.resources :messages, :new => { :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # - # map.resources :messages, :new => { :new => :any, :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # # --> /messages/new can be invoked via any request method - # - # map.resources :messages, :controller => "categories", - # :path_prefix => "/category/:category_id", - # :name_prefix => "category_" - # # --> GET /categories/7/messages/1 - # # has named route "category_message" - # - # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an - # HTTP POST on new_message_url will raise a RoutingError exception. The default route in - # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } - end - - # Creates named routes for implementing verb-oriented controllers for a singleton \resource. - # A singleton \resource is global to its current context. For unnested singleton \resources, - # the \resource is global to the current user visiting the application, such as a user's - # /account profile. For nested singleton \resources, the \resource is global to its parent - # \resource, such as a projects \resource that has_one :project_manager. - # The project_manager should be mapped as a singleton \resource under projects: - # - # map.resources :projects do |project| - # project.resource :project_manager - # end - # - # See +resources+ for general conventions. These are the main differences: - # * A singular name is given to map.resource. The default controller name is still taken from the plural name. - # * To specify a custom plural name, use the :plural option. There is no :singular option. - # * No default index route is created for the singleton \resource controller. - # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') - # - # For example: - # - # map.resource :account - # - # maps these actions in the Accounts controller: - # - # class AccountsController < ActionController::Base - # # GET new_account_url - # def new - # # return an HTML form for describing the new account - # end - # - # # POST account_url - # def create - # # create an account - # end - # - # # GET account_url - # def show - # # find and return the account - # end - # - # # GET edit_account_url - # def edit - # # return an HTML form for editing the account - # end - # - # # PUT account_url - # def update - # # find and update the account - # end - # - # # DELETE account_url - # def destroy - # # delete the account - # end - # end - # - # Along with the routes themselves, +resource+ generates named routes for - # use in controllers and views. map.resource :account produces - # these named routes and helpers: - # - # Named Route Helpers - # ============ ============================================= - # account account_url, hash_for_account_url, - # account_path, hash_for_account_path - # - # new_account new_account_url, hash_for_new_account_url, - # new_account_path, hash_for_new_account_path - # - # edit_account edit_account_url, hash_for_edit_account_url, - # edit_account_path, hash_for_edit_account_path - def resource(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } - end - - private - def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_default_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - end - end - - def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - map_default_singleton_actions(map, resource) - end - end - - def map_associations(resource, options) - map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] - - path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" - name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" - - Array(options[:has_one]).each do |association| - resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) - end - end - - def map_has_many_associations(resource, associations, options) - case associations - when Hash - associations.each do |association,has_many| - map_has_many_associations(resource, association, options.merge(:has_many => has_many)) - end - when Array - associations.each do |association| - map_has_many_associations(resource, association, options) - end - when Symbol, String - resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) - else - end - end - - def map_collection_actions(map, resource) - resource.collection_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= action - - map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) - end - end - end - end - - def map_default_collection_actions(map, resource) - index_route_name = "#{resource.name_prefix}#{resource.plural}" - - if resource.uncountable? - index_route_name << "_index" - end - - map_resource_routes(map, resource, :index, resource.path, index_route_name) - map_resource_routes(map, resource, :create, resource.path, index_route_name) - end - - def map_default_singleton_actions(map, resource) - map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") - end - - def map_new_actions(map, resource) - resource.new_methods.each do |method, actions| - actions.each do |action| - route_path = resource.new_path - route_name = "new_#{resource.name_prefix}#{resource.singular}" - - unless action == :new - route_path = "#{route_path}#{resource.action_separator}#{action}" - route_name = "#{action}_#{route_name}" - end - - map_resource_routes(map, resource, action, route_path, route_name, method) - end - end - end - - def map_member_actions(map, resource) - resource.member_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= Base.resources_path_names[action] || action - - map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) - end - end - end - - route_path = "#{resource.shallow_name_prefix}#{resource.singular}" - map_resource_routes(map, resource, :show, resource.member_path, route_path) - map_resource_routes(map, resource, :update, resource.member_path, route_path) - map_resource_routes(map, resource, :destroy, resource.member_path, route_path) - end - - def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) - if resource.has_action?(action) - action_options = action_options_for(action, resource, method, resource_options) - formatted_route_path = "#{route_path}.:format" - - if route_name && @set.named_routes[route_name.to_sym].nil? - map.named_route(route_name, formatted_route_path, action_options) - else - map.connect(formatted_route_path, action_options) - end - end - end - - def add_conditions_for(conditions, method) - returning({:conditions => conditions.dup}) do |options| - options[:conditions][:method] = method unless method == :any - end - end - - def action_options_for(action, resource, method = nil, resource_options = {}) - default_options = { :action => action.to_s } - require_id = !resource.kind_of?(SingletonResource) - force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) - - case default_options[:action] - when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) - when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) - when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) - when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) - when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) - else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb deleted file mode 100644 index 8135b5811e..0000000000 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ /dev/null @@ -1,699 +0,0 @@ -require 'rack/mount' -require 'forwardable' - -module ActionController - module Routing - class RouteSet #:nodoc: - NotFound = lambda { |env| - raise RoutingError, "No route matches #{env[::Rack::Mount::Const::PATH_INFO].inspect} with #{env.inspect}" - } - - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - - class Dispatcher - def initialize(options = {}) - defaults = options[:defaults] - @glob_param = options.delete(:glob) - end - - def call(env) - params = env[PARAMETERS_KEY] - merge_default_action!(params) - split_glob_param!(params) if @glob_param - params.each { |key, value| params[key] = URI.unescape(value) if value.is_a?(String) } - - if env['action_controller.recognize'] - [200, {}, params] - else - controller = controller(params) - controller.action(params[:action]).call(env) - end - end - - private - def controller(params) - if params && params.has_key?(:controller) - controller = "#{params[:controller].camelize}Controller" - ActiveSupport::Inflector.constantize(controller) - end - end - - def merge_default_action!(params) - params[:action] ||= 'index' - end - - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } - end - end - - module RouteExtensions - def segment_keys - conditions[:path_info].names.compact.map { |key| key.to_sym } - end - end - - # Mapper instances are used to build routes. The object passed to the draw - # block in config/routes.rb is a Mapper instance. - # - # Mapper instances have relatively few instance methods, in order to avoid - # clashes with named routes. - class Mapper #:doc: - include ActionController::Resources - - def initialize(set) #:nodoc: - @set = set - end - - # Create an unnamed route with the provided +path+ and +options+. See - # ActionController::Routing for an introduction to routes. - def connect(path, options = {}) - @set.add_route(path, options) - end - - # Creates a named route called "root" for matching the root level request. - def root(options = {}) - if options.is_a?(Symbol) - if source_route = @set.named_routes.routes[options] - options = source_route.defaults.merge({ :conditions => source_route.conditions }) - end - end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - @set.add_named_route(name, path, options) - end - - # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. - # Example: - # - # map.namespace(:admin) do |admin| - # admin.resources :products, - # :has_many => [ :tags, :images, :variants ] - # end - # - # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. - # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for - # Admin::TagsController. - def namespace(name, options = {}, &block) - if options[:namespace] - with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) - else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) - end - end - - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - @set.add_named_route(route_name, *args) - end - end - - # A NamedRouteCollection instance is a collection of named routes, and also - # maintains an anonymous module that can be used to install helpers for the - # named routes. - class NamedRouteCollection #:nodoc: - include Enumerable - attr_reader :routes, :helpers - - def initialize - clear! - end - - def clear! - @routes = {} - @helpers = [] - - @module ||= Module.new - @module.instance_methods.each do |selector| - @module.class_eval { remove_method selector } - end - end - - def add(name, route) - routes[name.to_sym] = route - define_named_route_methods(name, route) - end - - def get(name) - routes[name.to_sym] - end - - alias []= add - alias [] get - alias clear clear! - - def each - routes.each { |name, route| yield name, route } - self - end - - def names - routes.keys - end - - def length - routes.length - end - - def reset! - old_routes = routes.dup - clear! - old_routes.each do |name, route| - add(name, route) - end - end - - def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) - reset! if regenerate - Array(destinations).each do |dest| - dest.__send__(:include, @module) - end - end - - private - def url_helper_name(name, kind = :url) - :"#{name}_#{kind}" - end - - def hash_access_name(name, kind = :url) - :"hash_for_#{name}_#{kind}" - end - - def define_named_route_methods(name, route) - {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| - hash = route.defaults.merge(:use_route => name).merge(opts) - define_hash_access route, name, kind, hash - define_url_helper route, name, kind, hash - end - end - - def named_helper_module_eval(code, *args) - @module.module_eval(code, *args) - end - - def define_hash_access(route, name, kind, options) - selector = hash_access_name(name, kind) - named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks - def #{selector}(options = nil) # def hash_for_users_url(options = nil) - options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} - end # end - protected :#{selector} # protected :hash_for_users_url - end_eval - helpers << selector - end - - def define_url_helper(route, name, kind, options) - selector = url_helper_name(name, kind) - # The segment keys used for positional parameters - - hash_access_method = hash_access_name(name, kind) - - # allow ordered parameters to be associated with corresponding - # dynamic segments, so you can do - # - # foo_url(bar, baz, bang) - # - # instead of - # - # foo_url(:bar => bar, :baz => baz, :bang => bang) - # - # Also allow options hash, so you can do - # - # foo_url(bar, baz, bang, :sort_by => 'baz') - # - named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks - def #{selector}(*args) # def users_url(*args) - # - opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first - args.first || {} # args.first || {} - else # else - options = args.extract_options! # options = args.extract_options! - args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)| - h[k] = v # h[k] = v - h # h - end # end - options.merge(args) # options.merge(args) - end # end - # - url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts)) - # - end # end - #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL. - def formatted_#{selector}(*args) # def formatted_users_url(*args) - ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn( - "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " + - "Please pass format to the standard " + # "Please pass format to the standard " + - "#{selector} method instead.", caller) # "users_url method instead.", caller) - #{selector}(*args) # users_url(*args) - end # end - protected :#{selector} # protected :users_url - end_eval - helpers << selector - end - end - - attr_accessor :routes, :named_routes, :configuration_files - - def initialize - self.configuration_files = [] - - self.routes = [] - self.named_routes = NamedRouteCollection.new - - clear! - end - - def draw - clear! - yield Mapper.new(self) - @set.add_route(NotFound) - install_helpers - @set.freeze - end - - def clear! - routes.clear - named_routes.clear - @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) - end - - def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) - Array(destinations).each { |d| d.module_eval { include Helpers } } - named_routes.install(destinations, regenerate_code) - end - - def empty? - routes.empty? - end - - def add_configuration_file(path) - self.configuration_files << path - end - - # Deprecated accessor - def configuration_file=(path) - add_configuration_file(path) - end - - # Deprecated accessor - def configuration_file - configuration_files - end - - def load! - Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones - load_routes! - end - - # reload! will always force a reload whereas load checks the timestamp first - alias reload! load! - - def reload - if configuration_files.any? && @routes_last_modified - if routes_changed_at == @routes_last_modified - return # routes didn't change, don't reload - else - @routes_last_modified = routes_changed_at - end - end - - load! - end - - def load_routes! - if configuration_files.any? - configuration_files.each { |config| load(config) } - @routes_last_modified = routes_changed_at - else - draw do |map| - map.connect ":controller/:action/:id" - end - end - end - - def routes_changed_at - routes_changed_at = nil - - configuration_files.each do |config| - config_changed_at = File.stat(config).mtime - - if routes_changed_at.nil? || config_changed_at > routes_changed_at - routes_changed_at = config_changed_at - end - end - - routes_changed_at - end - - def add_route(path, options = {}) - options = options.dup - - if conditions = options.delete(:conditions) - conditions = conditions.dup - method = [conditions.delete(:method)].flatten.compact - method.map! { |m| - m = m.to_s.upcase - - if m == "HEAD" - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.downcase.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions" - end - - m - } - - if method.length > 1 - method = Regexp.union(*method) - elsif method.length == 1 - method = method.first - else - method = nil - end - end - - path_prefix = options.delete(:path_prefix) - name_prefix = options.delete(:name_prefix) - namespace = options.delete(:namespace) - - name = options.delete(:_name) - name = "#{name_prefix}#{name}" if name_prefix - - requirements = options.delete(:requirements) || {} - defaults = options.delete(:defaults) || {} - options.each do |k, v| - if v.is_a?(Regexp) - if value = options.delete(k) - requirements[k.to_sym] = value - end - else - value = options.delete(k) - defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param - end - end - - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - end - - possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } - requirements[:controller] ||= Regexp.union(*possible_names) - - if defaults[:controller] - defaults[:action] ||= 'index' - defaults[:controller] = defaults[:controller].to_s - defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace - end - - if defaults[:action] - defaults[:action] = defaults[:action].to_s - end - - if path.is_a?(String) - path = "#{path_prefix}/#{path}" if path_prefix - path = path.gsub('.:format', '(.:format)') - path = optionalize_trailing_dynamic_segments(path, requirements, defaults) - glob = $1.to_sym if path =~ /\/\*(\w+)$/ - path = ::Rack::Mount::Utils.normalize_path(path) - path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) - - if glob && !defaults[glob].blank? - raise RoutingError, "paths cannot have non-empty default values" - end - end - - app = Dispatcher.new(:defaults => defaults, :glob => glob) - - conditions = {} - conditions[:request_method] = method if method - conditions[:path_info] = path if path - - route = @set.add_route(app, conditions, defaults, name) - route.extend(RouteExtensions) - routes << route - route - end - - def add_named_route(name, path, options = {}) - options[:_name] = name - route = add_route(path, options) - named_routes[route.name] = route - route - end - - def options_as_params(options) - # If an explicit :controller was given, always make :action explicit - # too, so that action expiry works as expected for things like - # - # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) - # - # (the above is from the unit tests). In the above case, because the - # controller was explicitly given, but no action, the action is implied to - # be "index", not the recalled action of "show". - # - # great fun, eh? - - options_as_params = options.clone - options_as_params[:action] ||= 'index' if options[:controller] - options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] - options_as_params - end - - def build_expiry(options, recall) - recall.inject({}) do |expiry, (key, recalled_value)| - expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) - expiry - end - end - - # Generate the path indicated by the arguments, and return an array of - # the keys that were not used to generate it. - def extra_keys(options, recall={}) - generate_extras(options, recall).last - end - - def generate_extras(options, recall={}) - generate(options, recall, :generate_extras) - end - - def generate(options, recall = {}, method = :generate) - options, recall = options.dup, recall.dup - named_route = options.delete(:use_route) - - options = options_as_params(options) - expire_on = build_expiry(options, recall) - - recall[:action] ||= 'index' if options[:controller] || recall[:controller] - - if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) - options[:controller] = recall.delete(:controller) - - if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) - options[:action] = recall.delete(:action) - - if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) - options[:id] = recall.delete(:id) - end - end - end - - options[:controller] = options[:controller].to_s if options[:controller] - - if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ - old_parts = recall[:controller].split('/') - new_parts = options[:controller].split('/') - parts = old_parts[0..-(new_parts.length + 1)] + new_parts - options[:controller] = parts.join('/') - end - - options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ - - merged = options.merge(recall) - if options.has_key?(:action) && options[:action].nil? - options.delete(:action) - recall[:action] = 'index' - end - recall[:action] = options.delete(:action) if options[:action] == 'index' - - path = _uri(named_route, options, recall) - if path && method == :generate_extras - uri = URI(path) - extras = uri.query ? - Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : - [] - [uri.path, extras] - elsif path - path - else - raise RoutingError, "No route matches #{options.inspect}" - end - rescue Rack::Mount::RoutingError - raise RoutingError, "No route matches #{options.inspect}" - end - - def call(env) - @set.call(env) - rescue ActionController::RoutingError => e - raise e if env['action_controller.rescue_error'] == false - - method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] - - # Route was not recognized. Try to find out why (maybe wrong verb). - allows = HTTP_METHODS.select { |verb| - begin - recognize_path(path, {:method => verb}, false) - rescue ActionController::RoutingError - nil - end - } - - if !HTTP_METHODS.include?(method) - raise NotImplemented.new(*allows) - elsif !allows.empty? - raise MethodNotAllowed.new(*allows) - else - raise e - end - end - - def recognize(request) - params = recognize_path(request.path, extract_request_environment(request)) - request.path_parameters = params.with_indifferent_access - "#{params[:controller].to_s.camelize}Controller".constantize - end - - def recognize_path(path, environment = {}, rescue_error = true) - method = (environment[:method] || "GET").to_s.upcase - - begin - env = Rack::MockRequest.env_for(path, {:method => method}) - rescue URI::InvalidURIError => e - raise RoutingError, e.message - end - - env['action_controller.recognize'] = true - env['action_controller.rescue_error'] = rescue_error - status, headers, body = call(env) - body - end - - # Subclasses and plugins may override this method to extract further attributes - # from the request, for use by route conditions and such. - def extract_request_environment(request) - { :method => request.method } - end - - private - def _uri(named_route, params, recall) - params = URISegment.wrap_values(params) - recall = URISegment.wrap_values(recall) - - unless result = @set.generate(:path_info, named_route, params, recall) - return - end - - uri, params = result - params.each do |k, v| - if v._value - params[k] = v._value - else - params.delete(k) - end - end - - uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? - uri - end - - class URISegment < Struct.new(:_value, :_escape) - EXCLUDED = [:controller] - - def self.wrap_values(hash) - hash.inject({}) { |h, (k, v)| - h[k] = new(v, !EXCLUDED.include?(k.to_sym)) - h - } - end - - extend Forwardable - def_delegators :_value, :==, :eql?, :hash - - def to_param - @to_param ||= begin - if _value.is_a?(Array) - _value.map { |v| _escaped(v) }.join('/') - else - _escaped(_value) - end - end - end - alias_method :to_s, :to_param - - private - def _escaped(value) - v = value.respond_to?(:to_param) ? value.to_param : value - _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s - end - end - - def optionalize_trailing_dynamic_segments(path, requirements, defaults) - path = (path =~ /^\//) ? path.dup : "/#{path}" - optional, segments = true, [] - - required_segments = requirements.keys - required_segments -= defaults.keys.compact - - old_segments = path.split('/') - old_segments.shift - length = old_segments.length - - old_segments.reverse.each_with_index do |segment, index| - required_segments.each do |required| - if segment =~ /#{required}/ - optional = false - break - end - end - - if optional - if segment == ":id" && segments.include?(":action") - optional = false - elsif segment == ":controller" || segment == ":action" || segment == ":id" - # Ignore - elsif !(segment =~ /^:\w+$/) && - !(segment =~ /^:\w+\(\.:format\)$/) - optional = false - elsif segment =~ /^:(\w+)$/ - if defaults.has_key?($1.to_sym) - defaults.delete($1.to_sym) - else - optional = false - end - end - end - - if optional && index < length - 1 - segments.unshift('(/', segment) - segments.push(')') - elsif optional - segments.unshift('/(', segment) - segments.push(')') - else - segments.unshift('/', segment) - end - end - - segments.join - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/url_rewriter.rb b/actionpack/lib/action_controller/routing/url_rewriter.rb deleted file mode 100644 index 52b66c9303..0000000000 --- a/actionpack/lib/action_controller/routing/url_rewriter.rb +++ /dev/null @@ -1,204 +0,0 @@ -module ActionController - # In routes.rb one defines URL-to-controller mappings, but the reverse - # is also possible: an URL can be generated from one of your routing definitions. - # URL generation functionality is centralized in this module. - # - # See ActionController::Routing and ActionController::Resources for general - # information about routing and routes.rb. - # - # Tip: If you need to generate URLs from your models or some other place, - # then ActionController::UrlWriter is what you're looking for. Read on for - # an introduction. - # - # == URL generation from parameters - # - # As you may know, some functions - such as ActionController::Base#url_for - # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set - # of parameters. For example, you've probably had the chance to write code - # like this in one of your views: - # - # <%= link_to('Click here', :controller => 'users', - # :action => 'new', :message => 'Welcome!') %> - # - # #=> Generates a link to: /users/new?message=Welcome%21 - # - # link_to, and all other functions that require URL generation functionality, - # actually use ActionController::UrlWriter under the hood. And in particular, - # they use the ActionController::UrlWriter#url_for method. One can generate - # the same path as the above example by using the following code: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :only_path => true) - # # => "/users/new?message=Welcome%21" - # - # Notice the :only_path => true part. This is because UrlWriter has no - # information about the website hostname that your Rails app is serving. So if you - # want to include the hostname as well, then you must also pass the :host - # argument: - # - # include UrlWriter - # url_for(:controller => 'users', - # :action => 'new', - # :message => 'Welcome!', - # :host => 'www.example.com') # Changed this. - # # => "http://www.example.com/users/new?message=Welcome%21" - # - # By default, all controllers and views have access to a special version of url_for, - # that already knows what the current hostname is. So if you use url_for in your - # controllers or your views, then you don't need to explicitly pass the :host - # argument. - # - # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. - # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' - # in full. However, mailers don't have hostname information, and what's why you'll still - # have to specify the :host argument when generating URLs in mailers. - # - # - # == URL generation for named routes - # - # UrlWriter also allows one to access methods that have been auto-generated from - # named routes. For example, suppose that you have a 'users' resource in your - # routes.rb: - # - # map.resources :users - # - # This generates, among other things, the method users_path. By default, - # this method is accessible from your controllers, views and mailers. If you need - # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlWriter in your class: - # - # class User < ActiveRecord::Base - # include ActionController::UrlWriter - # - # def base_uri - # user_path(self) - # end - # end - # - # User.find(1).base_uri # => "/users/1" - module UrlWriter - def self.included(base) #:nodoc: - ActionController::Routing::Routes.install_helpers(base) - base.mattr_accessor :default_url_options - - # The default options for urls written by this writer. Typically a :host pair is provided. - base.default_url_options ||= {} - end - - # Generate a url based on the options provided, default_url_options and the - # routes defined in routes.rb. The following options are supported: - # - # * :only_path - If true, the relative url is returned. Defaults to +false+. - # * :protocol - The protocol to connect to. Defaults to 'http'. - # * :host - Specifies the host the link should be targeted at. - # If :only_path is false, this option must be - # provided either explicitly, or via +default_url_options+. - # * :port - Optionally specify the port to connect to. - # * :anchor - An anchor name to be appended to the path. - # * :skip_relative_url_root - If true, the url is not constructed using the - # +relative_url_root+ set in ActionController::Base.relative_url_root. - # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" - # - # Any other key (:controller, :action, etc.) given to - # +url_for+ is forwarded to the Routes module. - # - # Examples: - # - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' - # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' - def url_for(options) - options = self.class.default_url_options.merge(options) - - url = '' - - unless options.delete(:only_path) - url << (options.delete(:protocol) || 'http') - url << '://' unless url.match("://") - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - - url << options.delete(:host) - url << ":#{options.delete(:port)}" if options.key?(:port) - else - # Delete the unused options to prevent their appearance in the query string. - [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } - end - trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) - url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] - generated = Routing::Routes.generate(options, {}) - url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) - url << anchor if anchor - - url - end - end - - # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. - class UrlRewriter #:nodoc: - RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] - def initialize(request, parameters) - @request, @parameters = request, parameters - end - - def rewrite(options = {}) - rewrite_url(options) - end - - def to_str - "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" - end - - alias_method :to_s, :to_str - - private - # Given a path and options, returns a rewritten URL string - def rewrite_url(options) - rewritten_url = "" - - unless options[:only_path] - rewritten_url << (options[:protocol] || @request.protocol) - rewritten_url << "://" unless rewritten_url.match("://") - rewritten_url << rewrite_authentication(options) - rewritten_url << (options[:host] || @request.host_with_port) - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) - end - - path = rewrite_path(options) - rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] - rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor] - - rewritten_url - end - - # Given a Hash of options, generates a route - def rewrite_path(options) - options = options.symbolize_keys - options.update(options[:params].symbolize_keys) if options[:params] - - if (overwrite = options.delete(:overwrite_params)) - options.update(@parameters.symbolize_keys) - options.update(overwrite.symbolize_keys) - end - - RESERVED_OPTIONS.each { |k| options.delete(k) } - - # Generates the query string, too - Routing::Routes.generate(options, @request.symbolized_path_parameters) - end - - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" - else - "" - end - end - end -end diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb new file mode 100644 index 0000000000..52b66c9303 --- /dev/null +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -0,0 +1,204 @@ +module ActionController + # In routes.rb one defines URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. + # + # See ActionController::Routing and ActionController::Resources for general + # information about routing and routes.rb. + # + # Tip: If you need to generate URLs from your models or some other place, + # then ActionController::UrlWriter is what you're looking for. Read on for + # an introduction. + # + # == URL generation from parameters + # + # As you may know, some functions - such as ActionController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', :controller => 'users', + # :action => 'new', :message => 'Welcome!') %> + # + # #=> Generates a link to: /users/new?message=Welcome%21 + # + # link_to, and all other functions that require URL generation functionality, + # actually use ActionController::UrlWriter under the hood. And in particular, + # they use the ActionController::UrlWriter#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :only_path => true) + # # => "/users/new?message=Welcome%21" + # + # Notice the :only_path => true part. This is because UrlWriter has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the :host + # argument: + # + # include UrlWriter + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :host => 'www.example.com') # Changed this. + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the :host + # argument. + # + # For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for. + # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for' + # in full. However, mailers don't have hostname information, and what's why you'll still + # have to specify the :host argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlWriter also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # routes.rb: + # + # map.resources :users + # + # This generates, among other things, the method users_path. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that by including ActionController::UrlWriter in your class: + # + # class User < ActiveRecord::Base + # include ActionController::UrlWriter + # + # def base_uri + # user_path(self) + # end + # end + # + # User.find(1).base_uri # => "/users/1" + module UrlWriter + def self.included(base) #:nodoc: + ActionController::Routing::Routes.install_helpers(base) + base.mattr_accessor :default_url_options + + # The default options for urls written by this writer. Typically a :host pair is provided. + base.default_url_options ||= {} + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * :only_path - If true, the relative url is returned. Defaults to +false+. + # * :protocol - The protocol to connect to. Defaults to 'http'. + # * :host - Specifies the host the link should be targeted at. + # If :only_path is false, this option must be + # provided either explicitly, or via +default_url_options+. + # * :port - Optionally specify the port to connect to. + # * :anchor - An anchor name to be appended to the path. + # * :skip_relative_url_root - If true, the url is not constructed using the + # +relative_url_root+ set in ActionController::Base.relative_url_root. + # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" + # + # Any other key (:controller, :action, etc.) given to + # +url_for+ is forwarded to the Routes module. + # + # Examples: + # + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + def url_for(options) + options = self.class.default_url_options.merge(options) + + url = '' + + unless options.delete(:only_path) + url << (options.delete(:protocol) || 'http') + url << '://' unless url.match("://") + + raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] + + url << options.delete(:host) + url << ":#{options.delete(:port)}" if options.key?(:port) + else + # Delete the unused options to prevent their appearance in the query string. + [:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) } + end + trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash) + url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] + anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor] + generated = Routing::Routes.generate(options, {}) + url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated) + url << anchor if anchor + + url + end + end + + # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. + class UrlRewriter #:nodoc: + RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] + def initialize(request, parameters) + @request, @parameters = request, parameters + end + + def rewrite(options = {}) + rewrite_url(options) + end + + def to_str + "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" + end + + alias_method :to_s, :to_str + + private + # Given a path and options, returns a rewritten URL string + def rewrite_url(options) + rewritten_url = "" + + unless options[:only_path] + rewritten_url << (options[:protocol] || @request.protocol) + rewritten_url << "://" unless rewritten_url.match("://") + rewritten_url << rewrite_authentication(options) + rewritten_url << (options[:host] || @request.host_with_port) + rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) + end + + path = rewrite_path(options) + rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor] + + rewritten_url + end + + # Given a Hash of options, generates a route + def rewrite_path(options) + options = options.symbolize_keys + options.update(options[:params].symbolize_keys) if options[:params] + + if (overwrite = options.delete(:overwrite_params)) + options.update(@parameters.symbolize_keys) + options.update(overwrite.symbolize_keys) + end + + RESERVED_OPTIONS.each { |k| options.delete(k) } + + # Generates the query string, too + Routing::Routes.generate(options, @request.symbolized_path_parameters) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" + else + "" + end + end + end +end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 11cd812695..259814a322 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -41,6 +41,8 @@ module ActionDispatch autoload :Static, 'action_dispatch/middleware/static' autoload :StringCoercion, 'action_dispatch/middleware/string_coercion' + autoload :Routing, 'action_dispatch/routing' + autoload :Assertions, 'action_dispatch/testing/assertions' autoload :Integration, 'action_dispatch/testing/integration' autoload :IntegrationTest, 'action_dispatch/testing/integration' diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb new file mode 100644 index 0000000000..5a8df76326 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing.rb @@ -0,0 +1,381 @@ +require 'active_support/core_ext/object/conversions' +require 'active_support/core_ext/boolean/conversions' +require 'active_support/core_ext/nil/conversions' +require 'active_support/core_ext/regexp' + +module ActionDispatch + # == Routing + # + # The routing module provides URL rewriting in native Ruby. It's a way to + # redirect incoming requests to controllers and actions. This replaces + # mod_rewrite rules. Best of all, Rails' Routing works with any web server. + # Routes are defined in config/routes.rb. + # + # Consider the following route, installed by Rails when you generate your + # application: + # + # map.connect ':controller/:action/:id' + # + # This route states that it expects requests to consist of a + # :controller followed by an :action that in turn is fed + # some :id. + # + # Suppose you get an incoming request for /blog/edit/22, you'll end up + # with: + # + # params = { :controller => 'blog', + # :action => 'edit', + # :id => '22' + # } + # + # Think of creating routes as drawing a map for your requests. The map tells + # them where to go based on some predefined pattern: + # + # ActionController::Routing::Routes.draw do |map| + # Pattern 1 tells some request to go to one place + # Pattern 2 tell them to go to another + # ... + # end + # + # The following symbols are special: + # + # :controller maps to your controller name + # :action maps to an action with your controllers + # + # Other names simply map to a parameter as in the case of :id. + # + # == Route priority + # + # Not all routes are created equally. Routes have priority defined by the + # order of appearance of the routes in the config/routes.rb file. The priority goes + # from top to bottom. The last route in that file is at the lowest priority + # and will be applied last. If no route matches, 404 is returned. + # + # Within blocks, the empty pattern is at the highest priority. + # In practice this works out nicely: + # + # ActionController::Routing::Routes.draw do |map| + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # end + # map.connect ':controller/:action/:view' + # end + # + # In this case, invoking blog controller (with an URL like '/blog/') + # without parameters will activate the 'list' action by default. + # + # == Defaults routes and default parameters + # + # Setting a default route is straightforward in Rails - you simply append a + # Hash at the end of your mapping to set any default parameters. + # + # Example: + # + # ActionController::Routing:Routes.draw do |map| + # map.connect ':controller/:action/:id', :controller => 'blog' + # end + # + # This sets up +blog+ as the default controller if no other is specified. + # This means visiting '/' would invoke the blog controller. + # + # More formally, you can include arbitrary parameters in the route, thus: + # + # map.connect ':controller/:action/:id', :action => 'show', :page => 'Dashboard' + # + # This will pass the :page parameter to all incoming requests that match this route. + # + # Note: The default routes, as provided by the Rails generator, make all actions in every + # controller accessible via GET requests. You should consider removing them or commenting + # them out if you're using named routes and resources. + # + # == Named routes + # + # Routes can be named with the syntax map.name_of_route options, + # allowing for easy reference within your source as +name_of_route_url+ + # for the full URL and +name_of_route_path+ for the URI path. + # + # Example: + # + # # In routes.rb + # map.login 'login', :controller => 'accounts', :action => 'login' + # + # # With render, redirect_to, tests, etc. + # redirect_to login_url + # + # Arguments can be passed as well. + # + # redirect_to show_item_path(:id => 25) + # + # Use map.root as a shorthand to name a route for the root path "". + # + # # In routes.rb + # map.root :controller => 'blogs' + # + # # would recognize http://www.example.com/ as + # params = { :controller => 'blogs', :action => 'index' } + # + # # and provide these named routes + # root_url # => 'http://www.example.com/' + # root_path # => '' + # + # You can also specify an already-defined named route in your map.root call: + # + # # In routes.rb + # map.new_session :controller => 'sessions', :action => 'new' + # map.root :new_session + # + # Note: when using +with_options+, the route is simply named after the + # method you call on the block parameter rather than map. + # + # # In routes.rb + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # blog.delete 'delete/:id', :action => 'delete' + # blog.edit 'edit/:id', :action => 'edit' + # end + # + # # provides named routes for show, delete, and edit + # link_to @article.title, show_path(:id => @article.id) + # + # == Pretty URLs + # + # Routes can generate pretty URLs. For example: + # + # map.connect 'articles/:year/:month/:day', + # :controller => 'articles', + # :action => 'find_by_date', + # :year => /\d{4}/, + # :month => /\d{1,2}/, + # :day => /\d{1,2}/ + # + # Using the route above, the URL "http://localhost:3000/articles/2005/11/06" + # maps to + # + # params = {:year => '2005', :month => '11', :day => '06'} + # + # == Regular Expressions and parameters + # You can specify a regular expression to define a format for a parameter. + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :postalcode => /\d{5}(-\d{4})?/ + # + # or, more formally: + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ } + # + # Formats can include the 'ignorecase' and 'extended syntax' regular + # expression modifiers: + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :postalcode => /hx\d\d\s\d[a-z]{2}/i + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show',:requirements => { + # :postalcode => /# Postcode format + # \d{5} #Prefix + # (-\d{4})? #Suffix + # /x + # } + # + # Using the multiline match modifier will raise an ArgumentError. + # Encoding regular expression modifiers are silently ignored. The + # match will always use the default encoding or ASCII. + # + # == Route globbing + # + # Specifying *[string] as part of a rule like: + # + # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' + # + # will glob all remaining parts of the route that were not recognized earlier. + # The globbed values are in params[:path] as an array of path segments. + # + # == Route conditions + # + # With conditions you can define restrictions on routes. Currently the only valid condition is :method. + # + # * :method - Allows you to specify which method can access the route. Possible values are :post, + # :get, :put, :delete and :any. The default value is :any, + # :any means that any method can access the route. + # + # Example: + # + # map.connect 'post/:id', :controller => 'posts', :action => 'show', + # :conditions => { :method => :get } + # map.connect 'post/:id', :controller => 'posts', :action => 'create_comment', + # :conditions => { :method => :post } + # + # Now, if you POST to /posts/:id, it will route to the create_comment action. A GET on the same + # URL will route to the show action. + # + # == Reloading routes + # + # You can reload routes if you feel you must: + # + # ActionController::Routing::Routes.reload + # + # This will clear all named routes and reload routes.rb if the file has been modified from + # last load. To absolutely force reloading, use reload!. + # + # == Testing Routes + # + # The two main methods for testing your routes: + # + # === +assert_routing+ + # + # def test_movie_route_properly_splits + # opts = {:controller => "plugin", :action => "checkout", :id => "2"} + # assert_routing "plugin/checkout/2", opts + # end + # + # +assert_routing+ lets you test whether or not the route properly resolves into options. + # + # === +assert_recognizes+ + # + # def test_route_has_options + # opts = {:controller => "plugin", :action => "show", :id => "12"} + # assert_recognizes opts, "/plugins/show/12" + # end + # + # Note the subtle difference between the two: +assert_routing+ tests that + # a URL fits options while +assert_recognizes+ tests that a URL + # breaks into parameters properly. + # + # In tests you can simply pass the URL or named route to +get+ or +post+. + # + # def send_to_jail + # get '/jail' + # assert_response :success + # assert_template "jail/front" + # end + # + # def goes_to_login + # get login_url + # #... + # end + # + # == View a list of all your routes + # + # Run rake routes. + # + module Routing + autoload :Resources, 'action_dispatch/routing/resources' + autoload :RouteSet, 'action_dispatch/routing/route_set' + + SEPARATORS = %w( / . ? ) + + HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] + + ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set + + # The root paths which may contain controller files + mattr_accessor :controller_paths + self.controller_paths = [] + + # A helper module to hold URL related helpers. + module Helpers + include ActionController::PolymorphicRoutes + end + + class << self + # Expects an array of controller names as the first argument. + # Executes the passed block with only the named controllers named available. + # This method is used in internal Rails testing. + def with_controllers(names) + prior_controllers = @possible_controllers + use_controllers! names + yield + ensure + use_controllers! prior_controllers + end + + # Returns an array of paths, cleaned of double-slashes and relative path references. + # * "\\\" and "//" become "\\" or "/". + # * "/foo/bar/../config" becomes "/foo/config". + # The returned array is sorted by length, descending. + def normalize_paths(paths) + # do the hokey-pokey of path normalization... + paths = paths.collect do |path| + path = path. + gsub("//", "/"). # replace double / chars with a single + gsub("\\\\", "\\"). # replace double \ chars with a single + gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it + + # eliminate .. paths where possible + re = %r{[^/\\]+[/\\]\.\.[/\\]} + path.gsub!(re, "") while path.match(re) + path + end + + # start with longest path, first + paths = paths.uniq.sort_by { |path| - path.length } + end + + # Returns the array of controller names currently available to ActionController::Routing. + def possible_controllers + unless @possible_controllers + @possible_controllers = [] + + paths = controller_paths.select { |path| File.directory?(path) && path != "." } + + seen_paths = Hash.new {|h, k| h[k] = true; false} + normalize_paths(paths).each do |load_path| + Dir["#{load_path}/**/*_controller.rb"].collect do |path| + next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] + + controller_name = path[(load_path.length + 1)..-1] + + controller_name.gsub!(/_controller\.rb\Z/, '') + @possible_controllers << controller_name + end + end + + # remove duplicates + @possible_controllers.uniq! + end + @possible_controllers + end + + # Replaces the internal list of controllers available to ActionController::Routing with the passed argument. + # ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ]) + def use_controllers!(controller_names) + @possible_controllers = controller_names + end + + # Returns a controller path for a new +controller+ based on a +previous+ controller path. + # Handles 4 scenarios: + # + # * stay in the previous controller: + # controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion" + # + # * stay in the previous namespace: + # controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts" + # + # * forced move to the root namespace: + # controller_relative_to( "/posts", "groups/discussion" ) # => "posts" + # + # * previous namespace is root: + # controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts" + # + def controller_relative_to(controller, previous) + if controller.nil? then previous + elsif controller[0] == ?/ then controller[1..-1] + elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" + else controller + end + end + end + + ActiveSupport::Inflector.module_eval do + # Ensures that routes are reloaded when Rails inflections are updated. + def inflections_with_route_reloading(&block) + returning(inflections_without_route_reloading(&block)) { + ActionDispatch::Routing::Routes.reload! if block_given? + } + end + + alias_method_chain :inflections, :route_reloading + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/resources.rb b/actionpack/lib/action_dispatch/routing/resources.rb new file mode 100644 index 0000000000..ada0d0a648 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/resources.rb @@ -0,0 +1,687 @@ +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/try' + +module ActionDispatch + module Routing + # == Overview + # + # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a \resource (the noun). + # + # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. + # * POST - Creation of \resources. + # * PUT - Editing of attributes on a \resource. + # * DELETE - Deletion of a \resource. + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + module Resources + INHERITABLE_OPTIONS = :namespace, :shallow + + class Resource #:nodoc: + DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy + + attr_reader :collection_methods, :member_methods, :new_methods + attr_reader :path_prefix, :name_prefix, :path_segment + attr_reader :plural, :singular + attr_reader :options + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + @path_segment = options.delete(:as) || @plural + + @options = options + + arrange_actions + add_default_actions + set_allowed_actions + set_prefixes + end + + def controller + @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + end + + def requirements(with_id = false) + @requirements ||= @options[:requirements] || {} + @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + + with_id ? @requirements.merge(@id_requirement) : @requirements + end + + def conditions + @conditions ||= @options[:conditions] || {} + end + + def path + @path ||= "#{path_prefix}/#{path_segment}" + end + + def new_path + new_action = self.options[:path_names][:new] if self.options[:path_names] + new_action ||= ActionController::Base.resources_path_names[:new] + @new_path ||= "#{path}/#{new_action}" + end + + def shallow_path_prefix + @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix + end + + def member_path + @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" + end + + def nesting_path_prefix + @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" + end + + def shallow_name_prefix + @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix + end + + def nesting_name_prefix + "#{shallow_name_prefix}#{singular}_" + end + + def action_separator + @action_separator ||= ActionController::Base.resource_action_separator + end + + def uncountable? + @singular.to_s == @plural.to_s + end + + def has_action?(action) + !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) + end + + protected + def arrange_actions + @collection_methods = arrange_actions_by_methods(options.delete(:collection)) + @member_methods = arrange_actions_by_methods(options.delete(:member)) + @new_methods = arrange_actions_by_methods(options.delete(:new)) + end + + def add_default_actions + add_default_action(member_methods, :get, :edit) + add_default_action(new_methods, :get, :new) + end + + def set_allowed_actions + only, except = @options.values_at(:only, :except) + @allowed_actions ||= {} + + if only == :all || except == :none + only = nil + except = [] + elsif only == :none || except == :all + only = [] + except = nil + end + + if only + @allowed_actions[:only] = Array(only).map {|a| a.to_sym } + elsif except + @allowed_actions[:except] = Array(except).map {|a| a.to_sym } + end + end + + def action_allowed?(action) + only, except = @allowed_actions.values_at(:only, :except) + (!only || only.include?(action)) && (!except || !except.include?(action)) + end + + def set_prefixes + @path_prefix = options.delete(:path_prefix) + @name_prefix = options.delete(:name_prefix) + end + + def arrange_actions_by_methods(actions) + (actions || {}).inject({}) do |flipped_hash, (key, value)| + (flipped_hash[value] ||= []) << key + flipped_hash + end + end + + def add_default_action(collection, method, action) + (collection[method] ||= []).unshift(action) + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options) + @singular = @plural = entity + options[:controller] ||= @singular.to_s.pluralize + super + end + + alias_method :shallow_path_prefix, :path_prefix + alias_method :shallow_name_prefix, :name_prefix + alias_method :member_path, :path + alias_method :nesting_path_prefix, :path + end + + # Creates named routes for implementing verb-oriented controllers + # for a collection \resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, +resources+ generates named routes for use in + # controllers and views. map.resources :messages produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a @message object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # or + # + # <% form_for @message do |f| %> + # + # which takes into account whether @message is a new record or not and generates the + # path and method accordingly. + # + # The +resources+ method accepts the following options to customize the resulting routes: + # * :collection - Add named routes for other actions that operate on the collection. + # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, + # an array of any of the previous, or :any if the method does not matter. + # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. + # * :member - Same as :collection, but for actions that operate on a specific member. + # * :new - Same as :collection, but for actions that operate on the new \resource action. + # * :controller - Specify the controller name for the routes. + # * :singular - Specify the singular name used in the member routes. + # * :requirements - Set custom routing parameter requirements; this is a hash of either + # regular expressions (which must match for the route to match) or extra parameters. For example: + # + # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } + # + # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. + # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. + # * :as - Specify a different \resource name to use in the URL path. For example: + # # products_path == '/productos' + # map.resources :products, :as => 'productos' do |product| + # # product_reviews_path(product) == '/productos/1234/comentarios' + # product.resources :product_reviews, :as => 'comentarios' + # end + # + # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. + # * :has_many - Same has :has_one, but for plural \resources. + # + # You may directly specify the routing association with +has_one+ and +has_many+ like: + # + # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] + # + # This is the same as: + # + # map.resources :notes do |notes| + # notes.resource :author + # notes.resources :comments + # notes.resources :attachments + # end + # + # * :path_names - Specify different path names for the actions. For example: + # # new_products_path == '/productos/nuevo' + # # bids_product_path(1) == '/productos/1/licitacoes' + # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } + # + # You can also set default action names from an environment, like this: + # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } + # + # * :path_prefix - Set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use +resources+ like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest +resources+ calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment \resources work the same, but must now include a value for :article_id. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # If you don't want to load all objects from the database you might want to use the article_id directly: + # + # articles_comments_url(@comment.article_id, @comment) + # + # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use :name_prefix to override the generic named routes in a nested \resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named \resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # * :shallow - If true, paths for nested resources which reference a specific member + # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. + # + # The :shallow option is inherited by any nested resource(s). + # + # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: + # + # map.resources :users, :shallow => true do |user| + # user.resources :posts do |post| + # post.resources :comments + # end + # end + # # --> GET /users/1/posts (maps to the PostsController#index action as usual) + # # also adds the usual named route called "user_posts" + # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) + # # also adds the named route called "post" + # # --> GET /posts/2/comments (maps to the CommentsController#index action) + # # also adds the named route called "post_comments" + # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) + # # also adds the named route called "comment" + # + # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: + # + # map.resources :users, :has_many => { :posts => :comments }, :shallow => true + # + # * :only and :except - Specify which of the seven default actions should be routed to. + # + # :only and :except may be set to :all, :none, an action name or a + # list of action names. By default, routes are generated for all seven actions. + # + # For example: + # + # map.resources :posts, :only => [:index, :show] do |post| + # post.resources :comments, :except => [:update, :destroy] + # end + # # --> GET /posts (maps to the PostsController#index action) + # # --> POST /posts (fails) + # # --> GET /posts/1 (maps to the PostsController#show action) + # # --> DELETE /posts/1 (fails) + # # --> POST /posts/1/comments (maps to the CommentsController#create action) + # # --> PUT /posts/1/comments/1 (fails) + # + # If map.resources is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on new_message_url will raise a RoutingError exception. The default route in + # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton \resource. + # A singleton \resource is global to its current context. For unnested singleton \resources, + # the \resource is global to the current user visiting the application, such as a user's + # /account profile. For nested singleton \resources, the \resource is global to its parent + # \resource, such as a projects \resource that has_one :project_manager. + # The project_manager should be mapped as a singleton \resource under projects: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See +resources+ for general conventions. These are the main differences: + # * A singular name is given to map.resource. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the :plural option. There is no :singular option. + # * No default index route is created for the singleton \resource controller. + # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, +resource+ generates named routes for + # use in controllers and views. map.resource :account produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + def resource(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + end + + private + def map_resource(entities, options = {}, &block) + resource = Resource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_default_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + end + end + + def map_singleton_resource(entities, options = {}, &block) + resource = SingletonResource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + map_default_singleton_actions(map, resource) + end + end + + def map_associations(resource, options) + map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] + + path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" + name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + + Array(options[:has_one]).each do |association| + resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) + end + end + + def map_has_many_associations(resource, associations, options) + case associations + when Hash + associations.each do |association,has_many| + map_has_many_associations(resource, association, options.merge(:has_many => has_many)) + end + when Array + associations.each do |association| + map_has_many_associations(resource, association, options) + end + when Symbol, String + resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) + else + end + end + + def map_collection_actions(map, resource) + resource.collection_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= action + + map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) + end + end + end + end + + def map_default_collection_actions(map, resource) + index_route_name = "#{resource.name_prefix}#{resource.plural}" + + if resource.uncountable? + index_route_name << "_index" + end + + map_resource_routes(map, resource, :index, resource.path, index_route_name) + map_resource_routes(map, resource, :create, resource.path, index_route_name) + end + + def map_default_singleton_actions(map, resource) + map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") + end + + def map_new_actions(map, resource) + resource.new_methods.each do |method, actions| + actions.each do |action| + route_path = resource.new_path + route_name = "new_#{resource.name_prefix}#{resource.singular}" + + unless action == :new + route_path = "#{route_path}#{resource.action_separator}#{action}" + route_name = "#{action}_#{route_name}" + end + + map_resource_routes(map, resource, action, route_path, route_name, method) + end + end + end + + def map_member_actions(map, resource) + resource.member_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= ActionController::Base.resources_path_names[action] || action + + map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) + end + end + end + + route_path = "#{resource.shallow_name_prefix}#{resource.singular}" + map_resource_routes(map, resource, :show, resource.member_path, route_path) + map_resource_routes(map, resource, :update, resource.member_path, route_path) + map_resource_routes(map, resource, :destroy, resource.member_path, route_path) + end + + def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) + if resource.has_action?(action) + action_options = action_options_for(action, resource, method, resource_options) + formatted_route_path = "#{route_path}.:format" + + if route_name && @set.named_routes[route_name.to_sym].nil? + map.named_route(route_name, formatted_route_path, action_options) + else + map.connect(formatted_route_path, action_options) + end + end + end + + def add_conditions_for(conditions, method) + returning({:conditions => conditions.dup}) do |options| + options[:conditions][:method] = method unless method == :any + end + end + + def action_options_for(action, resource, method = nil, resource_options = {}) + default_options = { :action => action.to_s } + require_id = !resource.kind_of?(SingletonResource) + force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) + + case default_options[:action] + when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) + when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) + when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) + when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) + when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) + else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb new file mode 100644 index 0000000000..9e40108d00 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -0,0 +1,699 @@ +require 'rack/mount' +require 'forwardable' + +module ActionDispatch + module Routing + class RouteSet #:nodoc: + NotFound = lambda { |env| + raise ActionController::RoutingError, "No route matches #{env[::Rack::Mount::Const::PATH_INFO].inspect} with #{env.inspect}" + } + + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + + class Dispatcher + def initialize(options = {}) + defaults = options[:defaults] + @glob_param = options.delete(:glob) + end + + def call(env) + params = env[PARAMETERS_KEY] + merge_default_action!(params) + split_glob_param!(params) if @glob_param + params.each { |key, value| params[key] = URI.unescape(value) if value.is_a?(String) } + + if env['action_controller.recognize'] + [200, {}, params] + else + controller = controller(params) + controller.action(params[:action]).call(env) + end + end + + private + def controller(params) + if params && params.has_key?(:controller) + controller = "#{params[:controller].camelize}Controller" + ActiveSupport::Inflector.constantize(controller) + end + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end + end + + module RouteExtensions + def segment_keys + conditions[:path_info].names.compact.map { |key| key.to_sym } + end + end + + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + class Mapper #:doc: + include Routing::Resources + + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionDispatch::Routing for an introduction to routes. + def connect(path, options = {}) + @set.add_route(path, options) + end + + # Creates a named route called "root" for matching the root level request. + def root(options = {}) + if options.is_a?(Symbol) + if source_route = @set.named_routes.routes[options] + options = source_route.defaults.merge({ :conditions => source_route.conditions }) + end + end + named_route("root", '', options) + end + + def named_route(name, path, options = {}) #:nodoc: + @set.add_named_route(name, path, options) + end + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + def namespace(name, options = {}, &block) + if options[:namespace] + with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) + else + with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + end + end + + def method_missing(route_name, *args, &proc) #:nodoc: + super unless args.length >= 1 && proc.nil? + @set.add_named_route(route_name, *args) + end + end + + # A NamedRouteCollection instance is a collection of named routes, and also + # maintains an anonymous module that can be used to install helpers for the + # named routes. + class NamedRouteCollection #:nodoc: + include Enumerable + attr_reader :routes, :helpers + + def initialize + clear! + end + + def clear! + @routes = {} + @helpers = [] + + @module ||= Module.new + @module.instance_methods.each do |selector| + @module.class_eval { remove_method selector } + end + end + + def add(name, route) + routes[name.to_sym] = route + define_named_route_methods(name, route) + end + + def get(name) + routes[name.to_sym] + end + + alias []= add + alias [] get + alias clear clear! + + def each + routes.each { |name, route| yield name, route } + self + end + + def names + routes.keys + end + + def length + routes.length + end + + def reset! + old_routes = routes.dup + clear! + old_routes.each do |name, route| + add(name, route) + end + end + + def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) + reset! if regenerate + Array(destinations).each do |dest| + dest.__send__(:include, @module) + end + end + + private + def url_helper_name(name, kind = :url) + :"#{name}_#{kind}" + end + + def hash_access_name(name, kind = :url) + :"hash_for_#{name}_#{kind}" + end + + def define_named_route_methods(name, route) + {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| + hash = route.defaults.merge(:use_route => name).merge(opts) + define_hash_access route, name, kind, hash + define_url_helper route, name, kind, hash + end + end + + def named_helper_module_eval(code, *args) + @module.module_eval(code, *args) + end + + def define_hash_access(route, name, kind, options) + selector = hash_access_name(name, kind) + named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(options = nil) # def hash_for_users_url(options = nil) + options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} + end # end + protected :#{selector} # protected :hash_for_users_url + end_eval + helpers << selector + end + + def define_url_helper(route, name, kind, options) + selector = url_helper_name(name, kind) + # The segment keys used for positional parameters + + hash_access_method = hash_access_name(name, kind) + + # allow ordered parameters to be associated with corresponding + # dynamic segments, so you can do + # + # foo_url(bar, baz, bang) + # + # instead of + # + # foo_url(:bar => bar, :baz => baz, :bang => bang) + # + # Also allow options hash, so you can do + # + # foo_url(bar, baz, bang, :sort_by => 'baz') + # + named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(*args) # def users_url(*args) + # + opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first + args.first || {} # args.first || {} + else # else + options = args.extract_options! # options = args.extract_options! + args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)| + h[k] = v # h[k] = v + h # h + end # end + options.merge(args) # options.merge(args) + end # end + # + url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts)) + # + end # end + #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL. + def formatted_#{selector}(*args) # def formatted_users_url(*args) + ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn( + "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " + + "Please pass format to the standard " + # "Please pass format to the standard " + + "#{selector} method instead.", caller) # "users_url method instead.", caller) + #{selector}(*args) # users_url(*args) + end # end + protected :#{selector} # protected :users_url + end_eval + helpers << selector + end + end + + attr_accessor :routes, :named_routes, :configuration_files + + def initialize + self.configuration_files = [] + + self.routes = [] + self.named_routes = NamedRouteCollection.new + + clear! + end + + def draw + clear! + yield Mapper.new(self) + @set.add_route(NotFound) + install_helpers + @set.freeze + end + + def clear! + routes.clear + named_routes.clear + @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) + end + + def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) + Array(destinations).each { |d| d.module_eval { include Helpers } } + named_routes.install(destinations, regenerate_code) + end + + def empty? + routes.empty? + end + + def add_configuration_file(path) + self.configuration_files << path + end + + # Deprecated accessor + def configuration_file=(path) + add_configuration_file(path) + end + + # Deprecated accessor + def configuration_file + configuration_files + end + + def load! + Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones + load_routes! + end + + # reload! will always force a reload whereas load checks the timestamp first + alias reload! load! + + def reload + if configuration_files.any? && @routes_last_modified + if routes_changed_at == @routes_last_modified + return # routes didn't change, don't reload + else + @routes_last_modified = routes_changed_at + end + end + + load! + end + + def load_routes! + if configuration_files.any? + configuration_files.each { |config| load(config) } + @routes_last_modified = routes_changed_at + else + draw do |map| + map.connect ":controller/:action/:id" + end + end + end + + def routes_changed_at + routes_changed_at = nil + + configuration_files.each do |config| + config_changed_at = File.stat(config).mtime + + if routes_changed_at.nil? || config_changed_at > routes_changed_at + routes_changed_at = config_changed_at + end + end + + routes_changed_at + end + + def add_route(path, options = {}) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) + + if glob && !defaults[glob].blank? + raise ActionController::RoutingError, "paths cannot have non-empty default values" + end + end + + app = Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + route = @set.add_route(app, conditions, defaults, name) + route.extend(RouteExtensions) + routes << route + route + end + + def add_named_route(name, path, options = {}) + options[:_name] = name + route = add_route(path, options) + named_routes[route.name] = route + route + end + + def options_as_params(options) + # If an explicit :controller was given, always make :action explicit + # too, so that action expiry works as expected for things like + # + # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + # + # (the above is from the unit tests). In the above case, because the + # controller was explicitly given, but no action, the action is implied to + # be "index", not the recalled action of "show". + # + # great fun, eh? + + options_as_params = options.clone + options_as_params[:action] ||= 'index' if options[:controller] + options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] + options_as_params + end + + def build_expiry(options, recall) + recall.inject({}) do |expiry, (key, recalled_value)| + expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) + expiry + end + end + + # Generate the path indicated by the arguments, and return an array of + # the keys that were not used to generate it. + def extra_keys(options, recall={}) + generate_extras(options, recall).last + end + + def generate_extras(options, recall={}) + generate(options, recall, :generate_extras) + end + + def generate(options, recall = {}, method = :generate) + options, recall = options.dup, recall.dup + named_route = options.delete(:use_route) + + options = options_as_params(options) + expire_on = build_expiry(options, recall) + + recall[:action] ||= 'index' if options[:controller] || recall[:controller] + + if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) + options[:controller] = recall.delete(:controller) + + if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) + options[:action] = recall.delete(:action) + + if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) + options[:id] = recall.delete(:id) + end + end + end + + options[:controller] = options[:controller].to_s if options[:controller] + + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ + old_parts = recall[:controller].split('/') + new_parts = options[:controller].split('/') + parts = old_parts[0..-(new_parts.length + 1)] + new_parts + options[:controller] = parts.join('/') + end + + options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ + + merged = options.merge(recall) + if options.has_key?(:action) && options[:action].nil? + options.delete(:action) + recall[:action] = 'index' + end + recall[:action] = options.delete(:action) if options[:action] == 'index' + + path = _uri(named_route, options, recall) + if path && method == :generate_extras + uri = URI(path) + extras = uri.query ? + Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : + [] + [uri.path, extras] + elsif path + path + else + raise ActionController::RoutingError, "No route matches #{options.inspect}" + end + rescue Rack::Mount::RoutingError + raise ActionController::RoutingError, "No route matches #{options.inspect}" + end + + def call(env) + @set.call(env) + rescue ActionController::RoutingError => e + raise e if env['action_controller.rescue_error'] == false + + method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] + + # Route was not recognized. Try to find out why (maybe wrong verb). + allows = HTTP_METHODS.select { |verb| + begin + recognize_path(path, {:method => verb}, false) + rescue ActionController::RoutingError + nil + end + } + + if !HTTP_METHODS.include?(method) + raise ActionController::NotImplemented.new(*allows) + elsif !allows.empty? + raise ActionController::MethodNotAllowed.new(*allows) + else + raise e + end + end + + def recognize(request) + params = recognize_path(request.path, extract_request_environment(request)) + request.path_parameters = params.with_indifferent_access + "#{params[:controller].to_s.camelize}Controller".constantize + end + + def recognize_path(path, environment = {}, rescue_error = true) + method = (environment[:method] || "GET").to_s.upcase + + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + + env['action_controller.recognize'] = true + env['action_controller.rescue_error'] = rescue_error + status, headers, body = call(env) + body + end + + # Subclasses and plugins may override this method to extract further attributes + # from the request, for use by route conditions and such. + def extract_request_environment(request) + { :method => request.method } + end + + private + def _uri(named_route, params, recall) + params = URISegment.wrap_values(params) + recall = URISegment.wrap_values(recall) + + unless result = @set.generate(:path_info, named_route, params, recall) + return + end + + uri, params = result + params.each do |k, v| + if v._value + params[k] = v._value + else + params.delete(k) + end + end + + uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? + uri + end + + class URISegment < Struct.new(:_value, :_escape) + EXCLUDED = [:controller] + + def self.wrap_values(hash) + hash.inject({}) { |h, (k, v)| + h[k] = new(v, !EXCLUDED.include?(k.to_sym)) + h + } + end + + extend Forwardable + def_delegators :_value, :==, :eql?, :hash + + def to_param + @to_param ||= begin + if _value.is_a?(Array) + _value.map { |v| _escaped(v) }.join('/') + else + _escaped(_value) + end + end + end + alias_method :to_s, :to_param + + private + def _escaped(value) + v = value.respond_to?(:to_param) ? value.to_param : value + _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s + end + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end + end + end +end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 5b47de19ae..4f1bafbad1 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -41,7 +41,7 @@ class ResourcesTest < ActionController::TestCase end def test_should_arrange_actions - resource = ActionController::Resources::Resource.new(:messages, + resource = ActionDispatch::Routing::Resources::Resource.new(:messages, :collection => { :rss => :get, :reorder => :post, :csv => :post }, :member => { :rss => :get, :atom => :get, :upload => :post, :fix => :post }, :new => { :preview => :get, :draft => :get }) @@ -54,18 +54,18 @@ class ResourcesTest < ActionController::TestCase end def test_should_resource_controller_name_equal_resource_name_by_default - resource = ActionController::Resources::Resource.new(:messages, {}) + resource = ActionDispatch::Routing::Resources::Resource.new(:messages, {}) assert_equal 'messages', resource.controller end def test_should_resource_controller_name_equal_controller_option - resource = ActionController::Resources::Resource.new(:messages, :controller => 'posts') + resource = ActionDispatch::Routing::Resources::Resource.new(:messages, :controller => 'posts') assert_equal 'posts', resource.controller end def test_should_all_singleton_paths_be_the_same [ :path, :nesting_path_prefix, :member_path ].each do |method| - resource = ActionController::Resources::SingletonResource.new(:messages, :path_prefix => 'admin') + resource = ActionDispatch::Routing::Resources::SingletonResource.new(:messages, :path_prefix => 'admin') assert_equal 'admin/messages', resource.send(method) end end @@ -121,7 +121,7 @@ class ResourcesTest < ActionController::TestCase end def test_override_paths_for_default_restful_actions - resource = ActionController::Resources::Resource.new(:messages, + resource = ActionDispatch::Routing::Resources::Resource.new(:messages, :path_names => {:new => 'nuevo', :edit => 'editar'}) assert_equal resource.new_path, "#{resource.path}/nuevo" end @@ -135,7 +135,7 @@ class ResourcesTest < ActionController::TestCase def test_with_custom_conditions with_restful_routing :messages, :conditions => { :subdomain => 'app' } do - assert ActionController::Routing::Routes.recognize_path("/messages", :method => :get, :subdomain => 'app') + assert ActionDispatch::Routing::Routes.recognize_path("/messages", :method => :get, :subdomain => 'app') end end -- cgit v1.2.3 From df68cae0c0837fbf23fdfc3f04162307ffa8f2c1 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 20 Oct 2009 10:46:27 -0500 Subject: Group together all the old routing dsl logic --- actionpack/lib/action_controller.rb | 1 - actionpack/lib/action_controller/deprecated.rb | 1 - actionpack/lib/action_dispatch/routing.rb | 2 +- actionpack/lib/action_dispatch/routing/mapper.rb | 879 +++++++++++++++++++++ .../lib/action_dispatch/routing/resources.rb | 687 ---------------- .../lib/action_dispatch/routing/route_set.rb | 206 +---- actionpack/test/controller/resources_test.rb | 10 +- 7 files changed, 887 insertions(+), 899 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/mapper.rb delete mode 100644 actionpack/lib/action_dispatch/routing/resources.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index b5091f39f9..9db1a71202 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -26,7 +26,6 @@ module ActionController autoload :PerformanceTest, 'action_controller/deprecated/performance_test' autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' - autoload :Resources, 'action_controller/deprecated' autoload :Routing, 'action_controller/deprecated' autoload :SessionManagement, 'action_controller/metal/session_management' autoload :TestCase, 'action_controller/testing/test_case' diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index 23fe6a4c3a..589061e77c 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,5 +1,4 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response ActionController::Routing = ActionDispatch::Routing -ActionDispatch::Resources = ActionDispatch::Routing::Resources ActionController::Routing::Routes = ActionDispatch::Routing::RouteSet.new diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 5a8df76326..0647d051cb 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -260,7 +260,7 @@ module ActionDispatch # Run rake routes. # module Routing - autoload :Resources, 'action_dispatch/routing/resources' + autoload :Mapper, 'action_dispatch/routing/mapper' autoload :RouteSet, 'action_dispatch/routing/route_set' SEPARATORS = %w( / . ? ) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb new file mode 100644 index 0000000000..44afbb9cd7 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -0,0 +1,879 @@ +module ActionDispatch + module Routing + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + # + # == Overview + # + # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a \resource (the noun). + # + # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. + # * POST - Creation of \resources. + # * PUT - Editing of attributes on a \resource. + # * DELETE - Deletion of a \resource. + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + class Mapper #:doc: + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionDispatch::Routing for an introduction to routes. + def connect(path, options = {}) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) + + if glob && !defaults[glob].blank? + raise ActionController::RoutingError, "paths cannot have non-empty default values" + end + end + + app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + @set.add_route(app, conditions, defaults, name) + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end + private :optionalize_trailing_dynamic_segments + + # Creates a named route called "root" for matching the root level request. + def root(options = {}) + if options.is_a?(Symbol) + if source_route = @set.named_routes.routes[options] + options = source_route.defaults.merge({ :conditions => source_route.conditions }) + end + end + named_route("root", '', options) + end + + def named_route(name, path, options = {}) #:nodoc: + options[:_name] = name + connect(path, options) + end + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + def namespace(name, options = {}, &block) + if options[:namespace] + with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) + else + with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + end + end + + def method_missing(route_name, *args, &proc) #:nodoc: + super unless args.length >= 1 && proc.nil? + named_route(route_name, *args) + end + + INHERITABLE_OPTIONS = :namespace, :shallow + + class Resource #:nodoc: + DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy + + attr_reader :collection_methods, :member_methods, :new_methods + attr_reader :path_prefix, :name_prefix, :path_segment + attr_reader :plural, :singular + attr_reader :options + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + @path_segment = options.delete(:as) || @plural + + @options = options + + arrange_actions + add_default_actions + set_allowed_actions + set_prefixes + end + + def controller + @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + end + + def requirements(with_id = false) + @requirements ||= @options[:requirements] || {} + @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + + with_id ? @requirements.merge(@id_requirement) : @requirements + end + + def conditions + @conditions ||= @options[:conditions] || {} + end + + def path + @path ||= "#{path_prefix}/#{path_segment}" + end + + def new_path + new_action = self.options[:path_names][:new] if self.options[:path_names] + new_action ||= ActionController::Base.resources_path_names[:new] + @new_path ||= "#{path}/#{new_action}" + end + + def shallow_path_prefix + @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix + end + + def member_path + @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" + end + + def nesting_path_prefix + @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" + end + + def shallow_name_prefix + @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix + end + + def nesting_name_prefix + "#{shallow_name_prefix}#{singular}_" + end + + def action_separator + @action_separator ||= ActionController::Base.resource_action_separator + end + + def uncountable? + @singular.to_s == @plural.to_s + end + + def has_action?(action) + !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) + end + + protected + def arrange_actions + @collection_methods = arrange_actions_by_methods(options.delete(:collection)) + @member_methods = arrange_actions_by_methods(options.delete(:member)) + @new_methods = arrange_actions_by_methods(options.delete(:new)) + end + + def add_default_actions + add_default_action(member_methods, :get, :edit) + add_default_action(new_methods, :get, :new) + end + + def set_allowed_actions + only, except = @options.values_at(:only, :except) + @allowed_actions ||= {} + + if only == :all || except == :none + only = nil + except = [] + elsif only == :none || except == :all + only = [] + except = nil + end + + if only + @allowed_actions[:only] = Array(only).map {|a| a.to_sym } + elsif except + @allowed_actions[:except] = Array(except).map {|a| a.to_sym } + end + end + + def action_allowed?(action) + only, except = @allowed_actions.values_at(:only, :except) + (!only || only.include?(action)) && (!except || !except.include?(action)) + end + + def set_prefixes + @path_prefix = options.delete(:path_prefix) + @name_prefix = options.delete(:name_prefix) + end + + def arrange_actions_by_methods(actions) + (actions || {}).inject({}) do |flipped_hash, (key, value)| + (flipped_hash[value] ||= []) << key + flipped_hash + end + end + + def add_default_action(collection, method, action) + (collection[method] ||= []).unshift(action) + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options) + @singular = @plural = entity + options[:controller] ||= @singular.to_s.pluralize + super + end + + alias_method :shallow_path_prefix, :path_prefix + alias_method :shallow_name_prefix, :name_prefix + alias_method :member_path, :path + alias_method :nesting_path_prefix, :path + end + + # Creates named routes for implementing verb-oriented controllers + # for a collection \resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, +resources+ generates named routes for use in + # controllers and views. map.resources :messages produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a @message object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # or + # + # <% form_for @message do |f| %> + # + # which takes into account whether @message is a new record or not and generates the + # path and method accordingly. + # + # The +resources+ method accepts the following options to customize the resulting routes: + # * :collection - Add named routes for other actions that operate on the collection. + # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, + # an array of any of the previous, or :any if the method does not matter. + # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. + # * :member - Same as :collection, but for actions that operate on a specific member. + # * :new - Same as :collection, but for actions that operate on the new \resource action. + # * :controller - Specify the controller name for the routes. + # * :singular - Specify the singular name used in the member routes. + # * :requirements - Set custom routing parameter requirements; this is a hash of either + # regular expressions (which must match for the route to match) or extra parameters. For example: + # + # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } + # + # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. + # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. + # * :as - Specify a different \resource name to use in the URL path. For example: + # # products_path == '/productos' + # map.resources :products, :as => 'productos' do |product| + # # product_reviews_path(product) == '/productos/1234/comentarios' + # product.resources :product_reviews, :as => 'comentarios' + # end + # + # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. + # * :has_many - Same has :has_one, but for plural \resources. + # + # You may directly specify the routing association with +has_one+ and +has_many+ like: + # + # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] + # + # This is the same as: + # + # map.resources :notes do |notes| + # notes.resource :author + # notes.resources :comments + # notes.resources :attachments + # end + # + # * :path_names - Specify different path names for the actions. For example: + # # new_products_path == '/productos/nuevo' + # # bids_product_path(1) == '/productos/1/licitacoes' + # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } + # + # You can also set default action names from an environment, like this: + # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } + # + # * :path_prefix - Set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use +resources+ like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest +resources+ calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment \resources work the same, but must now include a value for :article_id. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # If you don't want to load all objects from the database you might want to use the article_id directly: + # + # articles_comments_url(@comment.article_id, @comment) + # + # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use :name_prefix to override the generic named routes in a nested \resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named \resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # * :shallow - If true, paths for nested resources which reference a specific member + # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. + # + # The :shallow option is inherited by any nested resource(s). + # + # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: + # + # map.resources :users, :shallow => true do |user| + # user.resources :posts do |post| + # post.resources :comments + # end + # end + # # --> GET /users/1/posts (maps to the PostsController#index action as usual) + # # also adds the usual named route called "user_posts" + # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) + # # also adds the named route called "post" + # # --> GET /posts/2/comments (maps to the CommentsController#index action) + # # also adds the named route called "post_comments" + # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) + # # also adds the named route called "comment" + # + # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: + # + # map.resources :users, :has_many => { :posts => :comments }, :shallow => true + # + # * :only and :except - Specify which of the seven default actions should be routed to. + # + # :only and :except may be set to :all, :none, an action name or a + # list of action names. By default, routes are generated for all seven actions. + # + # For example: + # + # map.resources :posts, :only => [:index, :show] do |post| + # post.resources :comments, :except => [:update, :destroy] + # end + # # --> GET /posts (maps to the PostsController#index action) + # # --> POST /posts (fails) + # # --> GET /posts/1 (maps to the PostsController#show action) + # # --> DELETE /posts/1 (fails) + # # --> POST /posts/1/comments (maps to the CommentsController#create action) + # # --> PUT /posts/1/comments/1 (fails) + # + # If map.resources is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on new_message_url will raise a RoutingError exception. The default route in + # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton \resource. + # A singleton \resource is global to its current context. For unnested singleton \resources, + # the \resource is global to the current user visiting the application, such as a user's + # /account profile. For nested singleton \resources, the \resource is global to its parent + # \resource, such as a projects \resource that has_one :project_manager. + # The project_manager should be mapped as a singleton \resource under projects: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See +resources+ for general conventions. These are the main differences: + # * A singular name is given to map.resource. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the :plural option. There is no :singular option. + # * No default index route is created for the singleton \resource controller. + # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, +resource+ generates named routes for + # use in controllers and views. map.resource :account produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + def resource(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + end + + private + def map_resource(entities, options = {}, &block) + resource = Resource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_default_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + end + end + + def map_singleton_resource(entities, options = {}, &block) + resource = SingletonResource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + map_default_singleton_actions(map, resource) + end + end + + def map_associations(resource, options) + map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] + + path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" + name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + + Array(options[:has_one]).each do |association| + resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) + end + end + + def map_has_many_associations(resource, associations, options) + case associations + when Hash + associations.each do |association,has_many| + map_has_many_associations(resource, association, options.merge(:has_many => has_many)) + end + when Array + associations.each do |association| + map_has_many_associations(resource, association, options) + end + when Symbol, String + resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) + else + end + end + + def map_collection_actions(map, resource) + resource.collection_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= action + + map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) + end + end + end + end + + def map_default_collection_actions(map, resource) + index_route_name = "#{resource.name_prefix}#{resource.plural}" + + if resource.uncountable? + index_route_name << "_index" + end + + map_resource_routes(map, resource, :index, resource.path, index_route_name) + map_resource_routes(map, resource, :create, resource.path, index_route_name) + end + + def map_default_singleton_actions(map, resource) + map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") + end + + def map_new_actions(map, resource) + resource.new_methods.each do |method, actions| + actions.each do |action| + route_path = resource.new_path + route_name = "new_#{resource.name_prefix}#{resource.singular}" + + unless action == :new + route_path = "#{route_path}#{resource.action_separator}#{action}" + route_name = "#{action}_#{route_name}" + end + + map_resource_routes(map, resource, action, route_path, route_name, method) + end + end + end + + def map_member_actions(map, resource) + resource.member_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= ActionController::Base.resources_path_names[action] || action + + map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) + end + end + end + + route_path = "#{resource.shallow_name_prefix}#{resource.singular}" + map_resource_routes(map, resource, :show, resource.member_path, route_path) + map_resource_routes(map, resource, :update, resource.member_path, route_path) + map_resource_routes(map, resource, :destroy, resource.member_path, route_path) + end + + def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) + if resource.has_action?(action) + action_options = action_options_for(action, resource, method, resource_options) + formatted_route_path = "#{route_path}.:format" + + if route_name && @set.named_routes[route_name.to_sym].nil? + map.named_route(route_name, formatted_route_path, action_options) + else + map.connect(formatted_route_path, action_options) + end + end + end + + def add_conditions_for(conditions, method) + returning({:conditions => conditions.dup}) do |options| + options[:conditions][:method] = method unless method == :any + end + end + + def action_options_for(action, resource, method = nil, resource_options = {}) + default_options = { :action => action.to_s } + require_id = !resource.kind_of?(SingletonResource) + force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) + + case default_options[:action] + when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) + when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) + when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) + when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) + when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) + else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/resources.rb b/actionpack/lib/action_dispatch/routing/resources.rb deleted file mode 100644 index ada0d0a648..0000000000 --- a/actionpack/lib/action_dispatch/routing/resources.rb +++ /dev/null @@ -1,687 +0,0 @@ -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/try' - -module ActionDispatch - module Routing - # == Overview - # - # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, - # is something that can be pointed at and it will respond with a representation of the data requested. - # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application - # requests XML data. - # - # RESTful design is based on the assumption that there are four generic verbs that a user of an - # application can request from a \resource (the noun). - # - # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used - # denotes the type of action that should take place. - # - # === The Different Methods and their Usage - # - # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. - # * POST - Creation of \resources. - # * PUT - Editing of attributes on a \resource. - # * DELETE - Deletion of a \resource. - # - # === Examples - # - # # A GET request on the Posts resource is asking for all Posts - # GET /posts - # - # # A GET request on a single Post resource is asking for that particular Post - # GET /posts/1 - # - # # A POST request on the Posts resource is asking for a Post to be created with the supplied details - # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } - # - # # A PUT request on a single Post resource is asking for a Post to be updated - # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } - # - # # A DELETE request on a single Post resource is asking for it to be deleted - # DELETE /posts # with => { :id => 1 } - # - # By using the REST convention, users of our application can assume certain things about how the data - # is requested and how it is returned. Rails simplifies the routing part of RESTful design by - # supplying you with methods to create them in your routes.rb file. - # - # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer - module Resources - INHERITABLE_OPTIONS = :namespace, :shallow - - class Resource #:nodoc: - DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy - - attr_reader :collection_methods, :member_methods, :new_methods - attr_reader :path_prefix, :name_prefix, :path_segment - attr_reader :plural, :singular - attr_reader :options - - def initialize(entities, options) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - - arrange_actions - add_default_actions - set_allowed_actions - set_prefixes - end - - def controller - @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" - end - - def requirements(with_id = false) - @requirements ||= @options[:requirements] || {} - @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } - - with_id ? @requirements.merge(@id_requirement) : @requirements - end - - def conditions - @conditions ||= @options[:conditions] || {} - end - - def path - @path ||= "#{path_prefix}/#{path_segment}" - end - - def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= ActionController::Base.resources_path_names[:new] - @new_path ||= "#{path}/#{new_action}" - end - - def shallow_path_prefix - @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix - end - - def member_path - @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" - end - - def nesting_path_prefix - @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" - end - - def shallow_name_prefix - @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix - end - - def nesting_name_prefix - "#{shallow_name_prefix}#{singular}_" - end - - def action_separator - @action_separator ||= ActionController::Base.resource_action_separator - end - - def uncountable? - @singular.to_s == @plural.to_s - end - - def has_action?(action) - !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) - end - - protected - def arrange_actions - @collection_methods = arrange_actions_by_methods(options.delete(:collection)) - @member_methods = arrange_actions_by_methods(options.delete(:member)) - @new_methods = arrange_actions_by_methods(options.delete(:new)) - end - - def add_default_actions - add_default_action(member_methods, :get, :edit) - add_default_action(new_methods, :get, :new) - end - - def set_allowed_actions - only, except = @options.values_at(:only, :except) - @allowed_actions ||= {} - - if only == :all || except == :none - only = nil - except = [] - elsif only == :none || except == :all - only = [] - except = nil - end - - if only - @allowed_actions[:only] = Array(only).map {|a| a.to_sym } - elsif except - @allowed_actions[:except] = Array(except).map {|a| a.to_sym } - end - end - - def action_allowed?(action) - only, except = @allowed_actions.values_at(:only, :except) - (!only || only.include?(action)) && (!except || !except.include?(action)) - end - - def set_prefixes - @path_prefix = options.delete(:path_prefix) - @name_prefix = options.delete(:name_prefix) - end - - def arrange_actions_by_methods(actions) - (actions || {}).inject({}) do |flipped_hash, (key, value)| - (flipped_hash[value] ||= []) << key - flipped_hash - end - end - - def add_default_action(collection, method, action) - (collection[method] ||= []).unshift(action) - end - end - - class SingletonResource < Resource #:nodoc: - def initialize(entity, options) - @singular = @plural = entity - options[:controller] ||= @singular.to_s.pluralize - super - end - - alias_method :shallow_path_prefix, :path_prefix - alias_method :shallow_name_prefix, :name_prefix - alias_method :member_path, :path - alias_method :nesting_path_prefix, :path - end - - # Creates named routes for implementing verb-oriented controllers - # for a collection \resource. - # - # For example: - # - # map.resources :messages - # - # will map the following actions in the corresponding controller: - # - # class MessagesController < ActionController::Base - # # GET messages_url - # def index - # # return all messages - # end - # - # # GET new_message_url - # def new - # # return an HTML form for describing a new message - # end - # - # # POST messages_url - # def create - # # create a new message - # end - # - # # GET message_url(:id => 1) - # def show - # # find and return a specific message - # end - # - # # GET edit_message_url(:id => 1) - # def edit - # # return an HTML form for editing a specific message - # end - # - # # PUT message_url(:id => 1) - # def update - # # find and update a specific message - # end - # - # # DELETE message_url(:id => 1) - # def destroy - # # delete a specific message - # end - # end - # - # Along with the routes themselves, +resources+ generates named routes for use in - # controllers and views. map.resources :messages produces the following named routes and helpers: - # - # Named Route Helpers - # ============ ===================================================== - # messages messages_url, hash_for_messages_url, - # messages_path, hash_for_messages_path - # - # message message_url(id), hash_for_message_url(id), - # message_path(id), hash_for_message_path(id) - # - # new_message new_message_url, hash_for_new_message_url, - # new_message_path, hash_for_new_message_path - # - # edit_message edit_message_url(id), hash_for_edit_message_url(id), - # edit_message_path(id), hash_for_edit_message_path(id) - # - # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: - # - # redirect_to :controller => 'messages', :action => 'index' - # # and - # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> - # - # now become: - # - # redirect_to messages_url - # # and - # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically - # - # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your - # form tags. The form helpers make this a little easier. For an update form with a @message object: - # - # <%= form_tag message_path(@message), :method => :put %> - # - # or - # - # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> - # - # or - # - # <% form_for @message do |f| %> - # - # which takes into account whether @message is a new record or not and generates the - # path and method accordingly. - # - # The +resources+ method accepts the following options to customize the resulting routes: - # * :collection - Add named routes for other actions that operate on the collection. - # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, - # an array of any of the previous, or :any if the method does not matter. - # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. - # * :member - Same as :collection, but for actions that operate on a specific member. - # * :new - Same as :collection, but for actions that operate on the new \resource action. - # * :controller - Specify the controller name for the routes. - # * :singular - Specify the singular name used in the member routes. - # * :requirements - Set custom routing parameter requirements; this is a hash of either - # regular expressions (which must match for the route to match) or extra parameters. For example: - # - # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } - # - # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. - # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. - # * :as - Specify a different \resource name to use in the URL path. For example: - # # products_path == '/productos' - # map.resources :products, :as => 'productos' do |product| - # # product_reviews_path(product) == '/productos/1234/comentarios' - # product.resources :product_reviews, :as => 'comentarios' - # end - # - # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. - # * :has_many - Same has :has_one, but for plural \resources. - # - # You may directly specify the routing association with +has_one+ and +has_many+ like: - # - # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] - # - # This is the same as: - # - # map.resources :notes do |notes| - # notes.resource :author - # notes.resources :comments - # notes.resources :attachments - # end - # - # * :path_names - Specify different path names for the actions. For example: - # # new_products_path == '/productos/nuevo' - # # bids_product_path(1) == '/productos/1/licitacoes' - # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } - # - # You can also set default action names from an environment, like this: - # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } - # - # * :path_prefix - Set a prefix to the routes with required route variables. - # - # Weblog comments usually belong to a post, so you might use +resources+ like: - # - # map.resources :articles - # map.resources :comments, :path_prefix => '/articles/:article_id' - # - # You can nest +resources+ calls to set this automatically: - # - # map.resources :articles do |article| - # article.resources :comments - # end - # - # The comment \resources work the same, but must now include a value for :article_id. - # - # article_comments_url(@article) - # article_comment_url(@article, @comment) - # - # article_comments_url(:article_id => @article) - # article_comment_url(:article_id => @article, :id => @comment) - # - # If you don't want to load all objects from the database you might want to use the article_id directly: - # - # articles_comments_url(@comment.article_id, @comment) - # - # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. - # Use this if you have named routes that may clash. - # - # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' - # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' - # - # You may also use :name_prefix to override the generic named routes in a nested \resource: - # - # map.resources :articles do |article| - # article.resources :comments, :name_prefix => nil - # end - # - # This will yield named \resources like so: - # - # comments_url(@article) - # comment_url(@article, @comment) - # - # * :shallow - If true, paths for nested resources which reference a specific member - # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. - # - # The :shallow option is inherited by any nested resource(s). - # - # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: - # - # map.resources :users, :shallow => true do |user| - # user.resources :posts do |post| - # post.resources :comments - # end - # end - # # --> GET /users/1/posts (maps to the PostsController#index action as usual) - # # also adds the usual named route called "user_posts" - # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) - # # also adds the named route called "post" - # # --> GET /posts/2/comments (maps to the CommentsController#index action) - # # also adds the named route called "post_comments" - # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) - # # also adds the named route called "comment" - # - # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: - # - # map.resources :users, :has_many => { :posts => :comments }, :shallow => true - # - # * :only and :except - Specify which of the seven default actions should be routed to. - # - # :only and :except may be set to :all, :none, an action name or a - # list of action names. By default, routes are generated for all seven actions. - # - # For example: - # - # map.resources :posts, :only => [:index, :show] do |post| - # post.resources :comments, :except => [:update, :destroy] - # end - # # --> GET /posts (maps to the PostsController#index action) - # # --> POST /posts (fails) - # # --> GET /posts/1 (maps to the PostsController#show action) - # # --> DELETE /posts/1 (fails) - # # --> POST /posts/1/comments (maps to the CommentsController#create action) - # # --> PUT /posts/1/comments/1 (fails) - # - # If map.resources is called with multiple resources, they all get the same options applied. - # - # Examples: - # - # map.resources :messages, :path_prefix => "/thread/:thread_id" - # # --> GET /thread/7/messages/1 - # - # map.resources :messages, :collection => { :rss => :get } - # # --> GET /messages/rss (maps to the #rss action) - # # also adds a named route called "rss_messages" - # - # map.resources :messages, :member => { :mark => :post } - # # --> POST /messages/1/mark (maps to the #mark action) - # # also adds a named route called "mark_message" - # - # map.resources :messages, :new => { :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # - # map.resources :messages, :new => { :new => :any, :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # # --> /messages/new can be invoked via any request method - # - # map.resources :messages, :controller => "categories", - # :path_prefix => "/category/:category_id", - # :name_prefix => "category_" - # # --> GET /categories/7/messages/1 - # # has named route "category_message" - # - # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an - # HTTP POST on new_message_url will raise a RoutingError exception. The default route in - # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } - end - - # Creates named routes for implementing verb-oriented controllers for a singleton \resource. - # A singleton \resource is global to its current context. For unnested singleton \resources, - # the \resource is global to the current user visiting the application, such as a user's - # /account profile. For nested singleton \resources, the \resource is global to its parent - # \resource, such as a projects \resource that has_one :project_manager. - # The project_manager should be mapped as a singleton \resource under projects: - # - # map.resources :projects do |project| - # project.resource :project_manager - # end - # - # See +resources+ for general conventions. These are the main differences: - # * A singular name is given to map.resource. The default controller name is still taken from the plural name. - # * To specify a custom plural name, use the :plural option. There is no :singular option. - # * No default index route is created for the singleton \resource controller. - # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') - # - # For example: - # - # map.resource :account - # - # maps these actions in the Accounts controller: - # - # class AccountsController < ActionController::Base - # # GET new_account_url - # def new - # # return an HTML form for describing the new account - # end - # - # # POST account_url - # def create - # # create an account - # end - # - # # GET account_url - # def show - # # find and return the account - # end - # - # # GET edit_account_url - # def edit - # # return an HTML form for editing the account - # end - # - # # PUT account_url - # def update - # # find and update the account - # end - # - # # DELETE account_url - # def destroy - # # delete the account - # end - # end - # - # Along with the routes themselves, +resource+ generates named routes for - # use in controllers and views. map.resource :account produces - # these named routes and helpers: - # - # Named Route Helpers - # ============ ============================================= - # account account_url, hash_for_account_url, - # account_path, hash_for_account_path - # - # new_account new_account_url, hash_for_new_account_url, - # new_account_path, hash_for_new_account_path - # - # edit_account edit_account_url, hash_for_edit_account_url, - # edit_account_path, hash_for_edit_account_path - def resource(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } - end - - private - def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_default_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - end - end - - def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - map_default_singleton_actions(map, resource) - end - end - - def map_associations(resource, options) - map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] - - path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" - name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" - - Array(options[:has_one]).each do |association| - resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) - end - end - - def map_has_many_associations(resource, associations, options) - case associations - when Hash - associations.each do |association,has_many| - map_has_many_associations(resource, association, options.merge(:has_many => has_many)) - end - when Array - associations.each do |association| - map_has_many_associations(resource, association, options) - end - when Symbol, String - resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) - else - end - end - - def map_collection_actions(map, resource) - resource.collection_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= action - - map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) - end - end - end - end - - def map_default_collection_actions(map, resource) - index_route_name = "#{resource.name_prefix}#{resource.plural}" - - if resource.uncountable? - index_route_name << "_index" - end - - map_resource_routes(map, resource, :index, resource.path, index_route_name) - map_resource_routes(map, resource, :create, resource.path, index_route_name) - end - - def map_default_singleton_actions(map, resource) - map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") - end - - def map_new_actions(map, resource) - resource.new_methods.each do |method, actions| - actions.each do |action| - route_path = resource.new_path - route_name = "new_#{resource.name_prefix}#{resource.singular}" - - unless action == :new - route_path = "#{route_path}#{resource.action_separator}#{action}" - route_name = "#{action}_#{route_name}" - end - - map_resource_routes(map, resource, action, route_path, route_name, method) - end - end - end - - def map_member_actions(map, resource) - resource.member_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= ActionController::Base.resources_path_names[action] || action - - map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) - end - end - end - - route_path = "#{resource.shallow_name_prefix}#{resource.singular}" - map_resource_routes(map, resource, :show, resource.member_path, route_path) - map_resource_routes(map, resource, :update, resource.member_path, route_path) - map_resource_routes(map, resource, :destroy, resource.member_path, route_path) - end - - def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) - if resource.has_action?(action) - action_options = action_options_for(action, resource, method, resource_options) - formatted_route_path = "#{route_path}.:format" - - if route_name && @set.named_routes[route_name.to_sym].nil? - map.named_route(route_name, formatted_route_path, action_options) - else - map.connect(formatted_route_path, action_options) - end - end - end - - def add_conditions_for(conditions, method) - returning({:conditions => conditions.dup}) do |options| - options[:conditions][:method] = method unless method == :any - end - end - - def action_options_for(action, resource, method = nil, resource_options = {}) - default_options = { :action => action.to_s } - require_id = !resource.kind_of?(SingletonResource) - force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) - - case default_options[:action] - when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) - when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) - when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) - when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) - when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) - else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 9e40108d00..a6e46b1c78 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -53,63 +53,6 @@ module ActionDispatch end end - # Mapper instances are used to build routes. The object passed to the draw - # block in config/routes.rb is a Mapper instance. - # - # Mapper instances have relatively few instance methods, in order to avoid - # clashes with named routes. - class Mapper #:doc: - include Routing::Resources - - def initialize(set) #:nodoc: - @set = set - end - - # Create an unnamed route with the provided +path+ and +options+. See - # ActionDispatch::Routing for an introduction to routes. - def connect(path, options = {}) - @set.add_route(path, options) - end - - # Creates a named route called "root" for matching the root level request. - def root(options = {}) - if options.is_a?(Symbol) - if source_route = @set.named_routes.routes[options] - options = source_route.defaults.merge({ :conditions => source_route.conditions }) - end - end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - @set.add_named_route(name, path, options) - end - - # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. - # Example: - # - # map.namespace(:admin) do |admin| - # admin.resources :products, - # :has_many => [ :tags, :images, :variants ] - # end - # - # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. - # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for - # Admin::TagsController. - def namespace(name, options = {}, &block) - if options[:namespace] - with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) - else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) - end - end - - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - @set.add_named_route(route_name, *args) - end - end - # A NamedRouteCollection instance is a collection of named routes, and also # maintains an anonymous module that can be used to install helpers for the # named routes. @@ -347,109 +290,14 @@ module ActionDispatch routes_changed_at end - def add_route(path, options = {}) - options = options.dup - - if conditions = options.delete(:conditions) - conditions = conditions.dup - method = [conditions.delete(:method)].flatten.compact - method.map! { |m| - m = m.to_s.upcase - - if m == "HEAD" - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.downcase.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions" - end - - m - } - - if method.length > 1 - method = Regexp.union(*method) - elsif method.length == 1 - method = method.first - else - method = nil - end - end - - path_prefix = options.delete(:path_prefix) - name_prefix = options.delete(:name_prefix) - namespace = options.delete(:namespace) - - name = options.delete(:_name) - name = "#{name_prefix}#{name}" if name_prefix - - requirements = options.delete(:requirements) || {} - defaults = options.delete(:defaults) || {} - options.each do |k, v| - if v.is_a?(Regexp) - if value = options.delete(k) - requirements[k.to_sym] = value - end - else - value = options.delete(k) - defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param - end - end - - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - end - - possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } - requirements[:controller] ||= Regexp.union(*possible_names) - - if defaults[:controller] - defaults[:action] ||= 'index' - defaults[:controller] = defaults[:controller].to_s - defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace - end - - if defaults[:action] - defaults[:action] = defaults[:action].to_s - end - - if path.is_a?(String) - path = "#{path_prefix}/#{path}" if path_prefix - path = path.gsub('.:format', '(.:format)') - path = optionalize_trailing_dynamic_segments(path, requirements, defaults) - glob = $1.to_sym if path =~ /\/\*(\w+)$/ - path = ::Rack::Mount::Utils.normalize_path(path) - path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) - - if glob && !defaults[glob].blank? - raise ActionController::RoutingError, "paths cannot have non-empty default values" - end - end - - app = Dispatcher.new(:defaults => defaults, :glob => glob) - - conditions = {} - conditions[:request_method] = method if method - conditions[:path_info] = path if path - + def add_route(app, conditions = {}, defaults = {}, name = nil) route = @set.add_route(app, conditions, defaults, name) route.extend(RouteExtensions) + named_routes[name] = route if name routes << route route end - def add_named_route(name, path, options = {}) - options[:_name] = name - route = add_route(path, options) - named_routes[route.name] = route - route - end - def options_as_params(options) # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -644,56 +492,6 @@ module ActionDispatch _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s end end - - def optionalize_trailing_dynamic_segments(path, requirements, defaults) - path = (path =~ /^\//) ? path.dup : "/#{path}" - optional, segments = true, [] - - required_segments = requirements.keys - required_segments -= defaults.keys.compact - - old_segments = path.split('/') - old_segments.shift - length = old_segments.length - - old_segments.reverse.each_with_index do |segment, index| - required_segments.each do |required| - if segment =~ /#{required}/ - optional = false - break - end - end - - if optional - if segment == ":id" && segments.include?(":action") - optional = false - elsif segment == ":controller" || segment == ":action" || segment == ":id" - # Ignore - elsif !(segment =~ /^:\w+$/) && - !(segment =~ /^:\w+\(\.:format\)$/) - optional = false - elsif segment =~ /^:(\w+)$/ - if defaults.has_key?($1.to_sym) - defaults.delete($1.to_sym) - else - optional = false - end - end - end - - if optional && index < length - 1 - segments.unshift('(/', segment) - segments.push(')') - elsif optional - segments.unshift('/(', segment) - segments.push(')') - else - segments.unshift('/', segment) - end - end - - segments.join - end end end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 4f1bafbad1..92373b5d26 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -41,7 +41,7 @@ class ResourcesTest < ActionController::TestCase end def test_should_arrange_actions - resource = ActionDispatch::Routing::Resources::Resource.new(:messages, + resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, :collection => { :rss => :get, :reorder => :post, :csv => :post }, :member => { :rss => :get, :atom => :get, :upload => :post, :fix => :post }, :new => { :preview => :get, :draft => :get }) @@ -54,18 +54,18 @@ class ResourcesTest < ActionController::TestCase end def test_should_resource_controller_name_equal_resource_name_by_default - resource = ActionDispatch::Routing::Resources::Resource.new(:messages, {}) + resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, {}) assert_equal 'messages', resource.controller end def test_should_resource_controller_name_equal_controller_option - resource = ActionDispatch::Routing::Resources::Resource.new(:messages, :controller => 'posts') + resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, :controller => 'posts') assert_equal 'posts', resource.controller end def test_should_all_singleton_paths_be_the_same [ :path, :nesting_path_prefix, :member_path ].each do |method| - resource = ActionDispatch::Routing::Resources::SingletonResource.new(:messages, :path_prefix => 'admin') + resource = ActionDispatch::Routing::Mapper::SingletonResource.new(:messages, :path_prefix => 'admin') assert_equal 'admin/messages', resource.send(method) end end @@ -121,7 +121,7 @@ class ResourcesTest < ActionController::TestCase end def test_override_paths_for_default_restful_actions - resource = ActionDispatch::Routing::Resources::Resource.new(:messages, + resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, :path_names => {:new => 'nuevo', :edit => 'editar'}) assert_equal resource.new_path, "#{resource.path}/nuevo" end -- cgit v1.2.3 From a1ce52effccae4851593f1d9b83ca9bf826bf338 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 20 Oct 2009 12:31:23 -0500 Subject: New routing dsl --- actionpack/lib/action_dispatch/routing.rb | 1 + .../action_dispatch/routing/deprecated_mapper.rb | 879 +++++++++++++++++ actionpack/lib/action_dispatch/routing/mapper.rb | 1004 +++++--------------- .../lib/action_dispatch/routing/route_set.rb | 4 +- actionpack/test/controller/resources_test.rb | 20 +- actionpack/test/controller/routing_test.rb | 332 +++++++ 6 files changed, 1443 insertions(+), 797 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/deprecated_mapper.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 0647d051cb..3803929847 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -260,6 +260,7 @@ module ActionDispatch # Run rake routes. # module Routing + autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper' autoload :Mapper, 'action_dispatch/routing/mapper' autoload :RouteSet, 'action_dispatch/routing/route_set' diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb new file mode 100644 index 0000000000..f2a1f10fa7 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -0,0 +1,879 @@ +module ActionDispatch + module Routing + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + # + # == Overview + # + # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a \resource (the noun). + # + # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. + # * POST - Creation of \resources. + # * PUT - Editing of attributes on a \resource. + # * DELETE - Deletion of a \resource. + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + class DeprecatedMapper #:doc: + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionDispatch::Routing for an introduction to routes. + def connect(path, options = {}) + options = options.dup + + if conditions = options.delete(:conditions) + conditions = conditions.dup + method = [conditions.delete(:method)].flatten.compact + method.map! { |m| + m = m.to_s.upcase + + if m == "HEAD" + raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" + end + + unless HTTP_METHODS.include?(m.downcase.to_sym) + raise ArgumentError, "Invalid HTTP method specified in route conditions" + end + + m + } + + if method.length > 1 + method = Regexp.union(*method) + elsif method.length == 1 + method = method.first + else + method = nil + end + end + + path_prefix = options.delete(:path_prefix) + name_prefix = options.delete(:name_prefix) + namespace = options.delete(:namespace) + + name = options.delete(:_name) + name = "#{name_prefix}#{name}" if name_prefix + + requirements = options.delete(:requirements) || {} + defaults = options.delete(:defaults) || {} + options.each do |k, v| + if v.is_a?(Regexp) + if value = options.delete(k) + requirements[k.to_sym] = value + end + else + value = options.delete(k) + defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param + end + end + + requirements.each do |_, requirement| + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + if defaults[:controller] + defaults[:action] ||= 'index' + defaults[:controller] = defaults[:controller].to_s + defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace + end + + if defaults[:action] + defaults[:action] = defaults[:action].to_s + end + + if path.is_a?(String) + path = "#{path_prefix}/#{path}" if path_prefix + path = path.gsub('.:format', '(.:format)') + path = optionalize_trailing_dynamic_segments(path, requirements, defaults) + glob = $1.to_sym if path =~ /\/\*(\w+)$/ + path = ::Rack::Mount::Utils.normalize_path(path) + path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) + + if glob && !defaults[glob].blank? + raise ActionController::RoutingError, "paths cannot have non-empty default values" + end + end + + app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) + + conditions = {} + conditions[:request_method] = method if method + conditions[:path_info] = path if path + + @set.add_route(app, conditions, defaults, name) + end + + def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: + path = (path =~ /^\//) ? path.dup : "/#{path}" + optional, segments = true, [] + + required_segments = requirements.keys + required_segments -= defaults.keys.compact + + old_segments = path.split('/') + old_segments.shift + length = old_segments.length + + old_segments.reverse.each_with_index do |segment, index| + required_segments.each do |required| + if segment =~ /#{required}/ + optional = false + break + end + end + + if optional + if segment == ":id" && segments.include?(":action") + optional = false + elsif segment == ":controller" || segment == ":action" || segment == ":id" + # Ignore + elsif !(segment =~ /^:\w+$/) && + !(segment =~ /^:\w+\(\.:format\)$/) + optional = false + elsif segment =~ /^:(\w+)$/ + if defaults.has_key?($1.to_sym) + defaults.delete($1.to_sym) + else + optional = false + end + end + end + + if optional && index < length - 1 + segments.unshift('(/', segment) + segments.push(')') + elsif optional + segments.unshift('/(', segment) + segments.push(')') + else + segments.unshift('/', segment) + end + end + + segments.join + end + private :optionalize_trailing_dynamic_segments + + # Creates a named route called "root" for matching the root level request. + def root(options = {}) + if options.is_a?(Symbol) + if source_route = @set.named_routes.routes[options] + options = source_route.defaults.merge({ :conditions => source_route.conditions }) + end + end + named_route("root", '', options) + end + + def named_route(name, path, options = {}) #:nodoc: + options[:_name] = name + connect(path, options) + end + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + def namespace(name, options = {}, &block) + if options[:namespace] + with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) + else + with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + end + end + + def method_missing(route_name, *args, &proc) #:nodoc: + super unless args.length >= 1 && proc.nil? + named_route(route_name, *args) + end + + INHERITABLE_OPTIONS = :namespace, :shallow + + class Resource #:nodoc: + DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy + + attr_reader :collection_methods, :member_methods, :new_methods + attr_reader :path_prefix, :name_prefix, :path_segment + attr_reader :plural, :singular + attr_reader :options + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + @path_segment = options.delete(:as) || @plural + + @options = options + + arrange_actions + add_default_actions + set_allowed_actions + set_prefixes + end + + def controller + @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + end + + def requirements(with_id = false) + @requirements ||= @options[:requirements] || {} + @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + + with_id ? @requirements.merge(@id_requirement) : @requirements + end + + def conditions + @conditions ||= @options[:conditions] || {} + end + + def path + @path ||= "#{path_prefix}/#{path_segment}" + end + + def new_path + new_action = self.options[:path_names][:new] if self.options[:path_names] + new_action ||= ActionController::Base.resources_path_names[:new] + @new_path ||= "#{path}/#{new_action}" + end + + def shallow_path_prefix + @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix + end + + def member_path + @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" + end + + def nesting_path_prefix + @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" + end + + def shallow_name_prefix + @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix + end + + def nesting_name_prefix + "#{shallow_name_prefix}#{singular}_" + end + + def action_separator + @action_separator ||= ActionController::Base.resource_action_separator + end + + def uncountable? + @singular.to_s == @plural.to_s + end + + def has_action?(action) + !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) + end + + protected + def arrange_actions + @collection_methods = arrange_actions_by_methods(options.delete(:collection)) + @member_methods = arrange_actions_by_methods(options.delete(:member)) + @new_methods = arrange_actions_by_methods(options.delete(:new)) + end + + def add_default_actions + add_default_action(member_methods, :get, :edit) + add_default_action(new_methods, :get, :new) + end + + def set_allowed_actions + only, except = @options.values_at(:only, :except) + @allowed_actions ||= {} + + if only == :all || except == :none + only = nil + except = [] + elsif only == :none || except == :all + only = [] + except = nil + end + + if only + @allowed_actions[:only] = Array(only).map {|a| a.to_sym } + elsif except + @allowed_actions[:except] = Array(except).map {|a| a.to_sym } + end + end + + def action_allowed?(action) + only, except = @allowed_actions.values_at(:only, :except) + (!only || only.include?(action)) && (!except || !except.include?(action)) + end + + def set_prefixes + @path_prefix = options.delete(:path_prefix) + @name_prefix = options.delete(:name_prefix) + end + + def arrange_actions_by_methods(actions) + (actions || {}).inject({}) do |flipped_hash, (key, value)| + (flipped_hash[value] ||= []) << key + flipped_hash + end + end + + def add_default_action(collection, method, action) + (collection[method] ||= []).unshift(action) + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options) + @singular = @plural = entity + options[:controller] ||= @singular.to_s.pluralize + super + end + + alias_method :shallow_path_prefix, :path_prefix + alias_method :shallow_name_prefix, :name_prefix + alias_method :member_path, :path + alias_method :nesting_path_prefix, :path + end + + # Creates named routes for implementing verb-oriented controllers + # for a collection \resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, +resources+ generates named routes for use in + # controllers and views. map.resources :messages produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a @message object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # or + # + # <% form_for @message do |f| %> + # + # which takes into account whether @message is a new record or not and generates the + # path and method accordingly. + # + # The +resources+ method accepts the following options to customize the resulting routes: + # * :collection - Add named routes for other actions that operate on the collection. + # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, + # an array of any of the previous, or :any if the method does not matter. + # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. + # * :member - Same as :collection, but for actions that operate on a specific member. + # * :new - Same as :collection, but for actions that operate on the new \resource action. + # * :controller - Specify the controller name for the routes. + # * :singular - Specify the singular name used in the member routes. + # * :requirements - Set custom routing parameter requirements; this is a hash of either + # regular expressions (which must match for the route to match) or extra parameters. For example: + # + # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } + # + # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. + # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. + # * :as - Specify a different \resource name to use in the URL path. For example: + # # products_path == '/productos' + # map.resources :products, :as => 'productos' do |product| + # # product_reviews_path(product) == '/productos/1234/comentarios' + # product.resources :product_reviews, :as => 'comentarios' + # end + # + # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. + # * :has_many - Same has :has_one, but for plural \resources. + # + # You may directly specify the routing association with +has_one+ and +has_many+ like: + # + # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] + # + # This is the same as: + # + # map.resources :notes do |notes| + # notes.resource :author + # notes.resources :comments + # notes.resources :attachments + # end + # + # * :path_names - Specify different path names for the actions. For example: + # # new_products_path == '/productos/nuevo' + # # bids_product_path(1) == '/productos/1/licitacoes' + # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } + # + # You can also set default action names from an environment, like this: + # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } + # + # * :path_prefix - Set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use +resources+ like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest +resources+ calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment \resources work the same, but must now include a value for :article_id. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # If you don't want to load all objects from the database you might want to use the article_id directly: + # + # articles_comments_url(@comment.article_id, @comment) + # + # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use :name_prefix to override the generic named routes in a nested \resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named \resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # * :shallow - If true, paths for nested resources which reference a specific member + # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. + # + # The :shallow option is inherited by any nested resource(s). + # + # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: + # + # map.resources :users, :shallow => true do |user| + # user.resources :posts do |post| + # post.resources :comments + # end + # end + # # --> GET /users/1/posts (maps to the PostsController#index action as usual) + # # also adds the usual named route called "user_posts" + # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) + # # also adds the named route called "post" + # # --> GET /posts/2/comments (maps to the CommentsController#index action) + # # also adds the named route called "post_comments" + # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) + # # also adds the named route called "comment" + # + # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: + # + # map.resources :users, :has_many => { :posts => :comments }, :shallow => true + # + # * :only and :except - Specify which of the seven default actions should be routed to. + # + # :only and :except may be set to :all, :none, an action name or a + # list of action names. By default, routes are generated for all seven actions. + # + # For example: + # + # map.resources :posts, :only => [:index, :show] do |post| + # post.resources :comments, :except => [:update, :destroy] + # end + # # --> GET /posts (maps to the PostsController#index action) + # # --> POST /posts (fails) + # # --> GET /posts/1 (maps to the PostsController#show action) + # # --> DELETE /posts/1 (fails) + # # --> POST /posts/1/comments (maps to the CommentsController#create action) + # # --> PUT /posts/1/comments/1 (fails) + # + # If map.resources is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on new_message_url will raise a RoutingError exception. The default route in + # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton \resource. + # A singleton \resource is global to its current context. For unnested singleton \resources, + # the \resource is global to the current user visiting the application, such as a user's + # /account profile. For nested singleton \resources, the \resource is global to its parent + # \resource, such as a projects \resource that has_one :project_manager. + # The project_manager should be mapped as a singleton \resource under projects: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See +resources+ for general conventions. These are the main differences: + # * A singular name is given to map.resource. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the :plural option. There is no :singular option. + # * No default index route is created for the singleton \resource controller. + # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, +resource+ generates named routes for + # use in controllers and views. map.resource :account produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + def resource(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + end + + private + def map_resource(entities, options = {}, &block) + resource = Resource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_default_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + end + end + + def map_singleton_resource(entities, options = {}, &block) + resource = SingletonResource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_associations(resource, options) + + if block_given? + with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) + end + + map_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + map_default_singleton_actions(map, resource) + end + end + + def map_associations(resource, options) + map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] + + path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" + name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + + Array(options[:has_one]).each do |association| + resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) + end + end + + def map_has_many_associations(resource, associations, options) + case associations + when Hash + associations.each do |association,has_many| + map_has_many_associations(resource, association, options.merge(:has_many => has_many)) + end + when Array + associations.each do |association| + map_has_many_associations(resource, association, options) + end + when Symbol, String + resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) + else + end + end + + def map_collection_actions(map, resource) + resource.collection_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= action + + map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) + end + end + end + end + + def map_default_collection_actions(map, resource) + index_route_name = "#{resource.name_prefix}#{resource.plural}" + + if resource.uncountable? + index_route_name << "_index" + end + + map_resource_routes(map, resource, :index, resource.path, index_route_name) + map_resource_routes(map, resource, :create, resource.path, index_route_name) + end + + def map_default_singleton_actions(map, resource) + map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") + end + + def map_new_actions(map, resource) + resource.new_methods.each do |method, actions| + actions.each do |action| + route_path = resource.new_path + route_name = "new_#{resource.name_prefix}#{resource.singular}" + + unless action == :new + route_path = "#{route_path}#{resource.action_separator}#{action}" + route_name = "#{action}_#{route_name}" + end + + map_resource_routes(map, resource, action, route_path, route_name, method) + end + end + end + + def map_member_actions(map, resource) + resource.member_methods.each do |method, actions| + actions.each do |action| + [method].flatten.each do |m| + action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) + action_path ||= ActionController::Base.resources_path_names[action] || action + + map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) + end + end + end + + route_path = "#{resource.shallow_name_prefix}#{resource.singular}" + map_resource_routes(map, resource, :show, resource.member_path, route_path) + map_resource_routes(map, resource, :update, resource.member_path, route_path) + map_resource_routes(map, resource, :destroy, resource.member_path, route_path) + end + + def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) + if resource.has_action?(action) + action_options = action_options_for(action, resource, method, resource_options) + formatted_route_path = "#{route_path}.:format" + + if route_name && @set.named_routes[route_name.to_sym].nil? + map.named_route(route_name, formatted_route_path, action_options) + else + map.connect(formatted_route_path, action_options) + end + end + end + + def add_conditions_for(conditions, method) + returning({:conditions => conditions.dup}) do |options| + options[:conditions][:method] = method unless method == :any + end + end + + def action_options_for(action, resource, method = nil, resource_options = {}) + default_options = { :action => action.to_s } + require_id = !resource.kind_of?(SingletonResource) + force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) + + case default_options[:action] + when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) + when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) + when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) + when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) + when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) + else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 44afbb9cd7..ab4193266a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,878 +1,312 @@ module ActionDispatch module Routing - # Mapper instances are used to build routes. The object passed to the draw - # block in config/routes.rb is a Mapper instance. - # - # Mapper instances have relatively few instance methods, in order to avoid - # clashes with named routes. - # - # == Overview - # - # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, - # is something that can be pointed at and it will respond with a representation of the data requested. - # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application - # requests XML data. - # - # RESTful design is based on the assumption that there are four generic verbs that a user of an - # application can request from a \resource (the noun). - # - # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used - # denotes the type of action that should take place. - # - # === The Different Methods and their Usage - # - # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. - # * POST - Creation of \resources. - # * PUT - Editing of attributes on a \resource. - # * DELETE - Deletion of a \resource. - # - # === Examples - # - # # A GET request on the Posts resource is asking for all Posts - # GET /posts - # - # # A GET request on a single Post resource is asking for that particular Post - # GET /posts/1 - # - # # A POST request on the Posts resource is asking for a Post to be created with the supplied details - # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } - # - # # A PUT request on a single Post resource is asking for a Post to be updated - # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } - # - # # A DELETE request on a single Post resource is asking for it to be deleted - # DELETE /posts # with => { :id => 1 } - # - # By using the REST convention, users of our application can assume certain things about how the data - # is requested and how it is returned. Rails simplifies the routing part of RESTful design by - # supplying you with methods to create them in your routes.rb file. - # - # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer - class Mapper #:doc: - def initialize(set) #:nodoc: - @set = set - end - - # Create an unnamed route with the provided +path+ and +options+. See - # ActionDispatch::Routing for an introduction to routes. - def connect(path, options = {}) - options = options.dup - - if conditions = options.delete(:conditions) - conditions = conditions.dup - method = [conditions.delete(:method)].flatten.compact - method.map! { |m| - m = m.to_s.upcase + class Mapper + module Resources + def resource(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} + + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resource(r, options) } + return self + end - if m == "HEAD" - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end + resource = resources.pop - unless HTTP_METHODS.include?(m.downcase.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions" + if @scope[:scope_level] == :resources + member do + resource(resource, options, &block) end - - m - } - - if method.length > 1 - method = Regexp.union(*method) - elsif method.length == 1 - method = method.first - else - method = nil + return self end - end - path_prefix = options.delete(:path_prefix) - name_prefix = options.delete(:name_prefix) - namespace = options.delete(:namespace) + controller(resource) do + namespace(resource) do + with_scope_level(:resource) do + yield if block_given? - name = options.delete(:_name) - name = "#{name_prefix}#{name}" if name_prefix - - requirements = options.delete(:requirements) || {} - defaults = options.delete(:defaults) || {} - options.each do |k, v| - if v.is_a?(Regexp) - if value = options.delete(k) - requirements[k.to_sym] = value + get "", :to => :show + post "", :to => :create + put "", :to => :update + delete "", :to => :destory + end end - else - value = options.delete(k) - defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param end - end - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end + self end - possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } - requirements[:controller] ||= Regexp.union(*possible_names) + def resources(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} - if defaults[:controller] - defaults[:action] ||= 'index' - defaults[:controller] = defaults[:controller].to_s - defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace - end - - if defaults[:action] - defaults[:action] = defaults[:action].to_s - end - - if path.is_a?(String) - path = "#{path_prefix}/#{path}" if path_prefix - path = path.gsub('.:format', '(.:format)') - path = optionalize_trailing_dynamic_segments(path, requirements, defaults) - glob = $1.to_sym if path =~ /\/\*(\w+)$/ - path = ::Rack::Mount::Utils.normalize_path(path) - path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) - - if glob && !defaults[glob].blank? - raise ActionController::RoutingError, "paths cannot have non-empty default values" + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resources(r, options) } + return self end - end - - app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) - conditions = {} - conditions[:request_method] = method if method - conditions[:path_info] = path if path - - @set.add_route(app, conditions, defaults, name) - end + resource = resources.pop - def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: - path = (path =~ /^\//) ? path.dup : "/#{path}" - optional, segments = true, [] - - required_segments = requirements.keys - required_segments -= defaults.keys.compact - - old_segments = path.split('/') - old_segments.shift - length = old_segments.length - - old_segments.reverse.each_with_index do |segment, index| - required_segments.each do |required| - if segment =~ /#{required}/ - optional = false - break + if @scope[:scope_level] == :resources + member do + resources(resource, options, &block) end + return self end - if optional - if segment == ":id" && segments.include?(":action") - optional = false - elsif segment == ":controller" || segment == ":action" || segment == ":id" - # Ignore - elsif !(segment =~ /^:\w+$/) && - !(segment =~ /^:\w+\(\.:format\)$/) - optional = false - elsif segment =~ /^:(\w+)$/ - if defaults.has_key?($1.to_sym) - defaults.delete($1.to_sym) - else - optional = false + controller(resource) do + namespace(resource) do + with_scope_level(:resources) do + yield if block_given? + + member do + get "", :to => :show + put "", :to => :update + delete "", :to => :destory + get "edit", :to => :edit + end + + collection do + get "", :to => :index + post "", :to => :create + get "new", :to => :new + end end end end - if optional && index < length - 1 - segments.unshift('(/', segment) - segments.push(')') - elsif optional - segments.unshift('/(', segment) - segments.push(')') - else - segments.unshift('/', segment) - end + self end - segments.join - end - private :optionalize_trailing_dynamic_segments - - # Creates a named route called "root" for matching the root level request. - def root(options = {}) - if options.is_a?(Symbol) - if source_route = @set.named_routes.routes[options] - options = source_route.defaults.merge({ :conditions => source_route.conditions }) + def collection + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use collection outside resources scope" end - end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - options[:_name] = name - connect(path, options) - end - # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. - # Example: - # - # map.namespace(:admin) do |admin| - # admin.resources :products, - # :has_many => [ :tags, :images, :variants ] - # end - # - # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. - # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for - # Admin::TagsController. - def namespace(name, options = {}, &block) - if options[:namespace] - with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) - else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + with_scope_level(:collection) do + yield + end end - end - - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - named_route(route_name, *args) - end - - INHERITABLE_OPTIONS = :namespace, :shallow - class Resource #:nodoc: - DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy - - attr_reader :collection_methods, :member_methods, :new_methods - attr_reader :path_prefix, :name_prefix, :path_segment - attr_reader :plural, :singular - attr_reader :options - - def initialize(entities, options) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - - arrange_actions - add_default_actions - set_allowed_actions - set_prefixes - end + def member + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use member outside resources scope" + end - def controller - @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + with_scope_level(:member) do + scope(":id") do + yield + end + end end - def requirements(with_id = false) - @requirements ||= @options[:requirements] || {} - @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.push(options) - with_id ? @requirements.merge(@id_requirement) : @requirements - end + case options.delete(:on) + when :collection + return collection { match(*args) } + when :member + return member { match(*args) } + end - def conditions - @conditions ||= @options[:conditions] || {} - end + if @scope[:scope_level] == :resources + raise ArgumentError, "can't define route directly in resources scope" + end - def path - @path ||= "#{path_prefix}/#{path_segment}" + super end - def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= ActionController::Base.resources_path_names[:new] - @new_path ||= "#{path}/#{new_action}" - end + private + def with_scope_level(kind) + old, @scope[:scope_level] = @scope[:scope_level], kind + yield + ensure + @scope[:scope_level] = old + end + end - def shallow_path_prefix - @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix - end + module Scoping + def scope(*args) + options = args.last.is_a?(Hash) ? args.pop : {} - def member_path - @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" - end + constraints = options.delete(:constraints) || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} + end + constraints, @scope[:constraints] = @scope[:constraints], (@scope[:constraints] || {}).merge(constraints) + blocks, @scope[:blocks] = @scope[:blocks], (@scope[:blocks] || []) + [block] + + options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options) + + path_set = controller_set = false + + case args.first + when String + path_set = true + path = args.first + path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}" + when Symbol + controller_set = true + controller = args.first + controller, @scope[:controller] = @scope[:controller], controller + end - def nesting_path_prefix - @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" - end + yield - def shallow_name_prefix - @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix + self + ensure + @scope[:path] = path if path_set + @scope[:controller] = controller if controller_set + @scope[:options] = options + @scope[:blocks] = blocks + @scope[:constraints] = constraints end - def nesting_name_prefix - "#{shallow_name_prefix}#{singular}_" + def controller(controller) + scope(controller.to_sym) { yield } end - def action_separator - @action_separator ||= ActionController::Base.resource_action_separator + def namespace(path) + scope(path.to_s) { yield } end - def uncountable? - @singular.to_s == @plural.to_s + def constraints(constraints = {}) + scope(:constraints => constraints) { yield } end + end - def has_action?(action) - !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) + class Constraints + def initialize(app, constraints = []) + @app, @constraints = app, constraints end - protected - def arrange_actions - @collection_methods = arrange_actions_by_methods(options.delete(:collection)) - @member_methods = arrange_actions_by_methods(options.delete(:member)) - @new_methods = arrange_actions_by_methods(options.delete(:new)) - end - - def add_default_actions - add_default_action(member_methods, :get, :edit) - add_default_action(new_methods, :get, :new) - end - - def set_allowed_actions - only, except = @options.values_at(:only, :except) - @allowed_actions ||= {} - - if only == :all || except == :none - only = nil - except = [] - elsif only == :none || except == :all - only = [] - except = nil - end - - if only - @allowed_actions[:only] = Array(only).map {|a| a.to_sym } - elsif except - @allowed_actions[:except] = Array(except).map {|a| a.to_sym } - end - end - - def action_allowed?(action) - only, except = @allowed_actions.values_at(:only, :except) - (!only || only.include?(action)) && (!except || !except.include?(action)) - end - - def set_prefixes - @path_prefix = options.delete(:path_prefix) - @name_prefix = options.delete(:name_prefix) - end + def call(env) + req = Rack::Request.new(env) - def arrange_actions_by_methods(actions) - (actions || {}).inject({}) do |flipped_hash, (key, value)| - (flipped_hash[value] ||= []) << key - flipped_hash + @constraints.each { |constraint| + if constraint.respond_to?(:matches?) && !constraint.matches?(req) + return Rack::Mount::Const::EXPECTATION_FAILED_RESPONSE + elsif constraint.respond_to?(:call) && !constraint.call(req) + return Rack::Mount::Const::EXPECTATION_FAILED_RESPONSE end - end + } - def add_default_action(collection, method, action) - (collection[method] ||= []).unshift(action) - end + @app.call(env) + end end - class SingletonResource < Resource #:nodoc: - def initialize(entity, options) - @singular = @plural = entity - options[:controller] ||= @singular.to_s.pluralize - super - end + def initialize(set) + @set = set + @scope = {} - alias_method :shallow_path_prefix, :path_prefix - alias_method :shallow_name_prefix, :name_prefix - alias_method :member_path, :path - alias_method :nesting_path_prefix, :path + extend Scoping + extend Resources end - # Creates named routes for implementing verb-oriented controllers - # for a collection \resource. - # - # For example: - # - # map.resources :messages - # - # will map the following actions in the corresponding controller: - # - # class MessagesController < ActionController::Base - # # GET messages_url - # def index - # # return all messages - # end - # - # # GET new_message_url - # def new - # # return an HTML form for describing a new message - # end - # - # # POST messages_url - # def create - # # create a new message - # end - # - # # GET message_url(:id => 1) - # def show - # # find and return a specific message - # end - # - # # GET edit_message_url(:id => 1) - # def edit - # # return an HTML form for editing a specific message - # end - # - # # PUT message_url(:id => 1) - # def update - # # find and update a specific message - # end - # - # # DELETE message_url(:id => 1) - # def destroy - # # delete a specific message - # end - # end - # - # Along with the routes themselves, +resources+ generates named routes for use in - # controllers and views. map.resources :messages produces the following named routes and helpers: - # - # Named Route Helpers - # ============ ===================================================== - # messages messages_url, hash_for_messages_url, - # messages_path, hash_for_messages_path - # - # message message_url(id), hash_for_message_url(id), - # message_path(id), hash_for_message_path(id) - # - # new_message new_message_url, hash_for_new_message_url, - # new_message_path, hash_for_new_message_path - # - # edit_message edit_message_url(id), hash_for_edit_message_url(id), - # edit_message_path(id), hash_for_edit_message_path(id) - # - # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: - # - # redirect_to :controller => 'messages', :action => 'index' - # # and - # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> - # - # now become: - # - # redirect_to messages_url - # # and - # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically - # - # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your - # form tags. The form helpers make this a little easier. For an update form with a @message object: - # - # <%= form_tag message_path(@message), :method => :put %> - # - # or - # - # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> - # - # or - # - # <% form_for @message do |f| %> - # - # which takes into account whether @message is a new record or not and generates the - # path and method accordingly. - # - # The +resources+ method accepts the following options to customize the resulting routes: - # * :collection - Add named routes for other actions that operate on the collection. - # Takes a hash of #{action} => #{method}, where method is :get/:post/:put/:delete, - # an array of any of the previous, or :any if the method does not matter. - # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. - # * :member - Same as :collection, but for actions that operate on a specific member. - # * :new - Same as :collection, but for actions that operate on the new \resource action. - # * :controller - Specify the controller name for the routes. - # * :singular - Specify the singular name used in the member routes. - # * :requirements - Set custom routing parameter requirements; this is a hash of either - # regular expressions (which must match for the route to match) or extra parameters. For example: - # - # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } - # - # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. - # * :conditions - Specify custom routing recognition conditions. \Resources sets the :method value for the method-specific routes. - # * :as - Specify a different \resource name to use in the URL path. For example: - # # products_path == '/productos' - # map.resources :products, :as => 'productos' do |product| - # # product_reviews_path(product) == '/productos/1234/comentarios' - # product.resources :product_reviews, :as => 'comentarios' - # end - # - # * :has_one - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. - # * :has_many - Same has :has_one, but for plural \resources. - # - # You may directly specify the routing association with +has_one+ and +has_many+ like: - # - # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] - # - # This is the same as: - # - # map.resources :notes do |notes| - # notes.resource :author - # notes.resources :comments - # notes.resources :attachments - # end - # - # * :path_names - Specify different path names for the actions. For example: - # # new_products_path == '/productos/nuevo' - # # bids_product_path(1) == '/productos/1/licitacoes' - # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } - # - # You can also set default action names from an environment, like this: - # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } - # - # * :path_prefix - Set a prefix to the routes with required route variables. - # - # Weblog comments usually belong to a post, so you might use +resources+ like: - # - # map.resources :articles - # map.resources :comments, :path_prefix => '/articles/:article_id' - # - # You can nest +resources+ calls to set this automatically: - # - # map.resources :articles do |article| - # article.resources :comments - # end - # - # The comment \resources work the same, but must now include a value for :article_id. - # - # article_comments_url(@article) - # article_comment_url(@article, @comment) - # - # article_comments_url(:article_id => @article) - # article_comment_url(:article_id => @article, :id => @comment) - # - # If you don't want to load all objects from the database you might want to use the article_id directly: - # - # articles_comments_url(@comment.article_id, @comment) - # - # * :name_prefix - Define a prefix for all generated routes, usually ending in an underscore. - # Use this if you have named routes that may clash. - # - # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' - # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' - # - # You may also use :name_prefix to override the generic named routes in a nested \resource: - # - # map.resources :articles do |article| - # article.resources :comments, :name_prefix => nil - # end - # - # This will yield named \resources like so: - # - # comments_url(@article) - # comment_url(@article, @comment) - # - # * :shallow - If true, paths for nested resources which reference a specific member - # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. - # - # The :shallow option is inherited by any nested resource(s). - # - # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: - # - # map.resources :users, :shallow => true do |user| - # user.resources :posts do |post| - # post.resources :comments - # end - # end - # # --> GET /users/1/posts (maps to the PostsController#index action as usual) - # # also adds the usual named route called "user_posts" - # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) - # # also adds the named route called "post" - # # --> GET /posts/2/comments (maps to the CommentsController#index action) - # # also adds the named route called "post_comments" - # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) - # # also adds the named route called "comment" - # - # You may also use :shallow in combination with the +has_one+ and +has_many+ shorthand notations like: - # - # map.resources :users, :has_many => { :posts => :comments }, :shallow => true - # - # * :only and :except - Specify which of the seven default actions should be routed to. - # - # :only and :except may be set to :all, :none, an action name or a - # list of action names. By default, routes are generated for all seven actions. - # - # For example: - # - # map.resources :posts, :only => [:index, :show] do |post| - # post.resources :comments, :except => [:update, :destroy] - # end - # # --> GET /posts (maps to the PostsController#index action) - # # --> POST /posts (fails) - # # --> GET /posts/1 (maps to the PostsController#show action) - # # --> DELETE /posts/1 (fails) - # # --> POST /posts/1/comments (maps to the CommentsController#create action) - # # --> PUT /posts/1/comments/1 (fails) - # - # If map.resources is called with multiple resources, they all get the same options applied. - # - # Examples: - # - # map.resources :messages, :path_prefix => "/thread/:thread_id" - # # --> GET /thread/7/messages/1 - # - # map.resources :messages, :collection => { :rss => :get } - # # --> GET /messages/rss (maps to the #rss action) - # # also adds a named route called "rss_messages" - # - # map.resources :messages, :member => { :mark => :post } - # # --> POST /messages/1/mark (maps to the #mark action) - # # also adds a named route called "mark_message" - # - # map.resources :messages, :new => { :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # - # map.resources :messages, :new => { :new => :any, :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # # --> /messages/new can be invoked via any request method - # - # map.resources :messages, :controller => "categories", - # :path_prefix => "/category/:category_id", - # :name_prefix => "category_" - # # --> GET /categories/7/messages/1 - # # has named route "category_message" - # - # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an - # HTTP POST on new_message_url will raise a RoutingError exception. The default route in - # config/routes.rb overrides this and allows invalid HTTP methods for \resource routes. - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } + def get(*args, &block) + map_method(:get, *args, &block) end - # Creates named routes for implementing verb-oriented controllers for a singleton \resource. - # A singleton \resource is global to its current context. For unnested singleton \resources, - # the \resource is global to the current user visiting the application, such as a user's - # /account profile. For nested singleton \resources, the \resource is global to its parent - # \resource, such as a projects \resource that has_one :project_manager. - # The project_manager should be mapped as a singleton \resource under projects: - # - # map.resources :projects do |project| - # project.resource :project_manager - # end - # - # See +resources+ for general conventions. These are the main differences: - # * A singular name is given to map.resource. The default controller name is still taken from the plural name. - # * To specify a custom plural name, use the :plural option. There is no :singular option. - # * No default index route is created for the singleton \resource controller. - # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') - # - # For example: - # - # map.resource :account - # - # maps these actions in the Accounts controller: - # - # class AccountsController < ActionController::Base - # # GET new_account_url - # def new - # # return an HTML form for describing the new account - # end - # - # # POST account_url - # def create - # # create an account - # end - # - # # GET account_url - # def show - # # find and return the account - # end - # - # # GET edit_account_url - # def edit - # # return an HTML form for editing the account - # end - # - # # PUT account_url - # def update - # # find and update the account - # end - # - # # DELETE account_url - # def destroy - # # delete the account - # end - # end - # - # Along with the routes themselves, +resource+ generates named routes for - # use in controllers and views. map.resource :account produces - # these named routes and helpers: - # - # Named Route Helpers - # ============ ============================================= - # account account_url, hash_for_account_url, - # account_path, hash_for_account_path - # - # new_account new_account_url, hash_for_new_account_url, - # new_account_path, hash_for_new_account_path - # - # edit_account edit_account_url, hash_for_edit_account_url, - # edit_account_path, hash_for_edit_account_path - def resource(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + def post(*args, &block) + map_method(:post, *args, &block) end - private - def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options) + def put(*args, &block) + map_method(:put, *args, &block) + end - with_options :controller => resource.controller do |map| - map_associations(resource, options) + def delete(*args, &block) + map_method(:delete, *args, &block) + end - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} - map_collection_actions(map, resource) - map_default_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - end + if args.length > 1 + args.each { |path| match(path, options) } + return self end - def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - map_default_singleton_actions(map, resource) - end + if args.first.is_a?(Symbol) + return match(args.first.to_s, options.merge(:to => args.first.to_sym)) end - def map_associations(resource, options) - map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] + path = args.first - path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" - name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + options = (@scope[:options] || {}).merge(options) + conditions, defaults = {}, {} - Array(options[:has_one]).each do |association| - resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) - end - end - - def map_has_many_associations(resource, associations, options) - case associations - when Hash - associations.each do |association,has_many| - map_has_many_associations(resource, association, options.merge(:has_many => has_many)) - end - when Array - associations.each do |association| - map_has_many_associations(resource, association, options) - end - when Symbol, String - resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) - else - end - end + path = nil if path == "" + path = Rack::Mount::Utils.normalize_path(path) if path + path = "#{@scope[:path]}#{path}" if @scope[:path] - def map_collection_actions(map, resource) - resource.collection_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= action + raise ArgumentError, "path is required" unless path - map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) - end - end - end + constraints = options[:constraints] || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} end + blocks = ((@scope[:blocks] || []) + [block]).compact + constraints = (@scope[:constraints] || {}).merge(constraints) + options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) } - def map_default_collection_actions(map, resource) - index_route_name = "#{resource.name_prefix}#{resource.plural}" - - if resource.uncountable? - index_route_name << "_index" - end + conditions[:path_info] = Rack::Mount::Strexp.compile(path, constraints, %w( / . ? )) - map_resource_routes(map, resource, :index, resource.path, index_route_name) - map_resource_routes(map, resource, :create, resource.path, index_route_name) - end + segment_keys = Rack::Mount::RegexpWithNamedGroups.new(conditions[:path_info]).names + constraints.reject! { |k, v| segment_keys.include?(k.to_s) } + conditions.merge!(constraints) - def map_default_singleton_actions(map, resource) - map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") + if via = options[:via] + via = Array(via).map { |m| m.to_s.upcase } + conditions[:request_method] = Regexp.union(*via) end - def map_new_actions(map, resource) - resource.new_methods.each do |method, actions| - actions.each do |action| - route_path = resource.new_path - route_name = "new_#{resource.name_prefix}#{resource.singular}" + defaults[:controller] = @scope[:controller].to_s if @scope[:controller] - unless action == :new - route_path = "#{route_path}#{resource.action_separator}#{action}" - route_name = "#{action}_#{route_name}" - end - - map_resource_routes(map, resource, action, route_path, route_name, method) - end - end + if options[:to].respond_to?(:call) + app = options[:to] + defaults.delete(:controller) + defaults.delete(:action) + elsif options[:to].is_a?(String) + defaults[:controller], defaults[:action] = options[:to].split('#') + elsif options[:to].is_a?(Symbol) + defaults[:action] = options[:to].to_s end + app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults) - def map_member_actions(map, resource) - resource.member_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= ActionController::Base.resources_path_names[action] || action - - map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) - end - end + if app.is_a?(Routing::RouteSet::Dispatcher) + unless defaults.include?(:controller) || segment_keys.include?("controller") + raise ArgumentError, "missing :controller" + end + unless defaults.include?(:action) || segment_keys.include?("action") + raise ArgumentError, "missing :action" end - - route_path = "#{resource.shallow_name_prefix}#{resource.singular}" - map_resource_routes(map, resource, :show, resource.member_path, route_path) - map_resource_routes(map, resource, :update, resource.member_path, route_path) - map_resource_routes(map, resource, :destroy, resource.member_path, route_path) end - def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) - if resource.has_action?(action) - action_options = action_options_for(action, resource, method, resource_options) - formatted_route_path = "#{route_path}.:format" + app = Constraints.new(app, blocks) if blocks.any? + @set.add_route(app, conditions, defaults, options[:as]) - if route_name && @set.named_routes[route_name.to_sym].nil? - map.named_route(route_name, formatted_route_path, action_options) - else - map.connect(formatted_route_path, action_options) - end - end - end + self + end - def add_conditions_for(conditions, method) - returning({:conditions => conditions.dup}) do |options| - options[:conditions][:method] = method unless method == :any - end - end + def redirect(path, options = {}) + status = options[:status] || 301 + lambda { |env| + req = Rack::Request.new(env) + url = req.scheme + '://' + req.host + path + [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] + } + end - def action_options_for(action, resource, method = nil, resource_options = {}) - default_options = { :action => action.to_s } - require_id = !resource.kind_of?(SingletonResource) - force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) - - case default_options[:action] - when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) - when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) - when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) - when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) - when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) - else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) - end + private + def map_method(method, *args, &block) + options = args.last.is_a?(Hash) ? args.pop : {} + options[:via] = method + args.push(options) + match(*args, &block) + self end end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index a6e46b1c78..90d7c208a5 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -208,9 +208,9 @@ module ActionDispatch clear! end - def draw + def draw(&block) clear! - yield Mapper.new(self) + Mapper.new(self).instance_exec(DeprecatedMapper.new(self), &block) @set.add_route(NotFound) install_helpers @set.freeze diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 92373b5d26..04e9acf855 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -41,7 +41,7 @@ class ResourcesTest < ActionController::TestCase end def test_should_arrange_actions - resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, + resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, :collection => { :rss => :get, :reorder => :post, :csv => :post }, :member => { :rss => :get, :atom => :get, :upload => :post, :fix => :post }, :new => { :preview => :get, :draft => :get }) @@ -54,18 +54,18 @@ class ResourcesTest < ActionController::TestCase end def test_should_resource_controller_name_equal_resource_name_by_default - resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, {}) + resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, {}) assert_equal 'messages', resource.controller end def test_should_resource_controller_name_equal_controller_option - resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, :controller => 'posts') + resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, :controller => 'posts') assert_equal 'posts', resource.controller end def test_should_all_singleton_paths_be_the_same [ :path, :nesting_path_prefix, :member_path ].each do |method| - resource = ActionDispatch::Routing::Mapper::SingletonResource.new(:messages, :path_prefix => 'admin') + resource = ActionDispatch::Routing::DeprecatedMapper::SingletonResource.new(:messages, :path_prefix => 'admin') assert_equal 'admin/messages', resource.send(method) end end @@ -121,7 +121,7 @@ class ResourcesTest < ActionController::TestCase end def test_override_paths_for_default_restful_actions - resource = ActionDispatch::Routing::Mapper::Resource.new(:messages, + resource = ActionDispatch::Routing::DeprecatedMapper::Resource.new(:messages, :path_names => {:new => 'nuevo', :edit => 'editar'}) assert_equal resource.new_path, "#{resource.path}/nuevo" end @@ -281,7 +281,7 @@ class ResourcesTest < ActionController::TestCase def test_with_member_action_and_requirement expected_options = {:controller => 'messages', :action => 'mark', :id => '1.1.1'} - + with_restful_routing(:messages, :requirements => {:id => /[0-9]\.[0-9]\.[0-9]/}, :member => { :mark => :get }) do assert_recognizes(expected_options, :path => 'messages/1.1.1/mark', :method => :get) end @@ -701,8 +701,8 @@ class ResourcesTest < ActionController::TestCase def test_should_not_allow_invalid_head_method_for_member_routes with_routing do |set| - set.draw do |map| - assert_raise(ArgumentError) do + assert_raise(ArgumentError) do + set.draw do |map| map.resources :messages, :member => {:something => :head} end end @@ -711,8 +711,8 @@ class ResourcesTest < ActionController::TestCase def test_should_not_allow_invalid_http_methods_for_member_routes with_routing do |set| - set.draw do |map| - assert_raise(ArgumentError) do + assert_raise(ArgumentError) do + set.draw do |map| map.resources :messages, :member => {:something => :invalid} end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 308e2a85b1..d7e4646df5 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -2046,3 +2046,335 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert true end end + +class TestRoutingMapper < ActiveSupport::TestCase + include Rack::Test::Methods + + SprocketsApp = lambda { |env| + [200, {"Content-Type" => "text/html"}, ["javascripts"]] + } + + class IpRestrictor + def self.matches?(request) + request.ip =~ /192\.168\.1\.1\d\d/ + end + end + + class Dispatcher + def self.new(*args) + lambda { |env| + params = env['action_dispatch.request.path_parameters'] + controller, action = params[:controller], params[:action] + [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] + } + end + end + old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, Dispatcher } + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + controller :sessions do + get 'login', :to => :new, :as => :login + post 'login', :to => :create + + delete 'logout', :to => :destroy, :as => :logout + end + + match 'account/login', :to => redirect("/login") + + match 'openid/login', :via => [:get, :post], :to => "openid#login" + + controller(:global) do + match 'global/:action' + match 'global/export', :to => :export, :as => :export_request + match 'global/hide_notice', :to => :hide_notice, :as => :hide_notice + match '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } + end + + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin', :to => "queenbee#index" + end + + constraints IpRestrictor do + get 'admin/accounts', :to => "queenbee#accounts" + end + + resources :projects, :controller => :project do + resources :involvements, :attachments + + resources :participants do + put :update_all, :on => :collection + end + + resources :companies do + resources :people + resource :avatar + end + + resources :images do + post :revise, :on => :member + end + + resources :people do + namespace ":access_token" do + resource :avatar + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end + end + + resources :posts do + get :archive, :toggle_view, :on => :collection + post :preview, :on => :member + + resource :subscription + + resources :comments do + post :preview, :on => :collection + end + end + end + + match 'sprockets.js', :to => SprocketsApp + + match 'people/:id/update', :to => 'people#update', :as => :update_person + match '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person + + # misc + match 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article + + namespace :account do + resource :subscription, :credit, :credit_card + end + + controller :articles do + scope 'articles' do + scope ':title', :title => /[a-z]+/, :as => :with_title do + match ':id', :to => :with_id + end + end + end + + scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do + resources :rooms + end + end + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + + def app + Routes + end + + def test_logout + delete '/logout' + assert_equal 'sessions#destroy', last_response.body + + # assert_equal '/logout', app.logout_path + end + + def test_login + get '/login' + assert_equal 'sessions#new', last_response.body + + post '/login' + assert_equal 'sessions#create', last_response.body + + # assert_equal '/login', app.login_path + end + + def test_login_redirect + get '/account/login' + assert_equal 301, last_response.status + assert_equal 'http://example.org/login', last_response.headers['Location'] + assert_equal 'Moved Permanently', last_response.body + end + + def test_openid + get '/openid/login' + assert_equal 'openid#login', last_response.body + + post '/openid/login' + assert_equal 'openid#login', last_response.body + end + + # def test_admin + # get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#index', last_response.body + # + # assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # + # get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#accounts', last_response.body + # + # assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # end + + def test_global + get '/global/dashboard' + assert_equal 'global#dashboard', last_response.body + + get '/global/export' + assert_equal 'global#export', last_response.body + + get '/global/hide_notice' + assert_equal 'global#hide_notice', last_response.body + + get '/export/123/foo.txt' + assert_equal 'global#export', last_response.body + + # assert_equal '/global/export', app.export_request_path + # assert_equal '/global/hide_notice', app.hide_notice_path + # assert_equal '/export/123/foo.txt', app.export_download_path(:id => 123, :file => 'foo.txt') + end + + def test_projects + get '/projects/1' + assert_equal 'projects#show', last_response.body + end + + def test_projects_involvements + get '/projects/1/involvements' + assert_equal 'involvements#index', last_response.body + + get '/projects/1/involvements/1' + assert_equal 'involvements#show', last_response.body + end + + def test_projects_attachments + get '/projects/1/attachments' + assert_equal 'attachments#index', last_response.body + end + + def test_projects_participants + get '/projects/1/participants' + assert_equal 'participants#index', last_response.body + + put '/projects/1/participants/update_all' + assert_equal 'participants#update_all', last_response.body + end + + def test_projects_companies + get '/projects/1/companies' + assert_equal 'companies#index', last_response.body + + get '/projects/1/companies/1/people' + assert_equal 'people#index', last_response.body + + get '/projects/1/companies/1/avatar' + assert_equal 'avatar#show', last_response.body + end + + def test_project_images + get '/projects/1/images' + assert_equal 'images#index', last_response.body + + post '/projects/1/images/1/revise' + assert_equal 'images#revise', last_response.body + end + + def test_projects_people + get '/projects/1/people' + assert_equal 'people#index', last_response.body + + get '/projects/1/people/1' + assert_equal 'people#show', last_response.body + + get '/projects/1/people/1/7a2dec8/avatar' + assert_equal 'avatar#show', last_response.body + + put '/projects/1/people/1/accessible_projects' + assert_equal 'people#accessible_projects', last_response.body + + post '/projects/1/people/1/resend' + assert_equal 'people#resend', last_response.body + + post '/projects/1/people/1/generate_new_password' + assert_equal 'people#generate_new_password', last_response.body + end + + def test_projects_posts + get '/projects/1/posts' + assert_equal 'posts#index', last_response.body + + get '/projects/1/posts/archive' + assert_equal 'posts#archive', last_response.body + + get '/projects/1/posts/toggle_view' + assert_equal 'posts#toggle_view', last_response.body + + post '/projects/1/posts/1/preview' + assert_equal 'posts#preview', last_response.body + + get '/projects/1/posts/1/subscription' + assert_equal 'subscription#show', last_response.body + + get '/projects/1/posts/1/comments' + assert_equal 'comments#index', last_response.body + + post '/projects/1/posts/1/comments/preview' + assert_equal 'comments#preview', last_response.body + end + + def test_sprockets + get '/sprockets.js' + assert_equal 'javascripts', last_response.body + end + + def test_update_person_route + get '/people/1/update' + assert_equal 'people#update', last_response.body + + # assert_equal '/people/1/update', app.update_person_path(:id => 1) + end + + def test_update_project_person + get '/projects/1/people/2/update' + assert_equal 'people#update', last_response.body + + # assert_equal '/projects/1/people/2/update', app.update_project_person_path(:project_id => 1, :id => 2) + end + + def test_articles_perma + get '/articles/2009/08/18/rails-3' + assert_equal 'articles#show', last_response.body + + # assert_equal '/articles/2009/8/18/rails-3', app.article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') + end + + def test_account_namespace + get '/account/subscription' + assert_equal 'subscription#show', last_response.body + + get '/account/credit' + assert_equal 'credit#show', last_response.body + + get '/account/credit_card' + assert_equal 'credit_card#show', last_response.body + end + + def test_articles_with_id + get '/articles/rails/1' + assert_equal 'articles#with_id', last_response.body + + assert_raise(ActionController::RoutingError) { get '/articles/123/1' } + + # assert_equal '/articles/rails/1', app.with_title_path(:title => 'rails', :id => 1) + end + + def test_access_token_rooms + get '/12345/rooms' + assert_equal 'rooms#index', last_response.body + + get '/12345/rooms/1' + assert_equal 'rooms#show', last_response.body + + get '/12345/rooms/1/edit' + assert_equal 'rooms#edit', last_response.body + end +end -- cgit v1.2.3 From a5c82a9dfb6d63bf90a3378da0b71d6ea592d7e3 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 20 Oct 2009 16:03:55 -0500 Subject: Start rewriting some internal tests to use the new routing dsl --- actionpack/lib/action_controller/metal.rb | 4 ++++ actionpack/test/controller/base_test.rb | 24 +++++++++++----------- actionpack/test/controller/integration_test.rb | 2 +- actionpack/test/controller/rescue_test.rb | 6 +++--- actionpack/test/controller/url_rewriter_test.rb | 8 +++----- actionpack/test/controller/verification_test.rb | 4 ++-- .../dispatch/request/json_params_parsing_test.rb | 2 +- .../dispatch/request/query_string_parsing_test.rb | 2 +- .../request/url_encoded_params_parsing_test.rb | 2 +- .../dispatch/request/xml_params_parsing_test.rb | 2 +- .../test/dispatch/session/cookie_store_test.rb | 2 +- .../test/dispatch/session/mem_cache_store_test.rb | 2 +- actionpack/test/template/test_test.rb | 3 +-- 13 files changed, 32 insertions(+), 31 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index e9007d3631..6f89bf5d67 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -109,6 +109,10 @@ module ActionController middleware_stack end + def self.call(env) + action(env['action_dispatch.request.path_parameters'][:action]).call(env) + end + # Return a rack endpoint for the given action. Memoize the endpoint, so # multiple calls into MyController.action will return the same object # for the same action. diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index b97ceb4594..b57550a69a 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -10,12 +10,12 @@ module Submodule def public_action render :nothing => true end - + hide_action :hidden_action def hidden_action raise "Noooo!" end - + def another_hidden_action end hide_action :another_hidden_action @@ -30,25 +30,25 @@ class NonEmptyController < ActionController::Base def public_action render :nothing => true end - + hide_action :hidden_action def hidden_action end end class MethodMissingController < ActionController::Base - + hide_action :shouldnt_be_called def shouldnt_be_called raise "NO WAY!" end - + protected - + def method_missing(selector) render :text => selector.to_s end - + end class DefaultUrlOptionsController < ActionController::Base @@ -79,7 +79,7 @@ class ControllerInstanceTests < Test::Unit::TestCase @empty = EmptyController.new @contained = Submodule::ContainedEmptyController.new @empty_controllers = [@empty, @contained, Submodule::SubclassedController.new] - + @non_empty_controllers = [NonEmptyController.new, Submodule::ContainedNonEmptyController.new] end @@ -135,24 +135,24 @@ class PerformActionTest < ActionController::TestCase rescue_action_in_public! end - + def test_get_on_priv_should_show_selector use_controller MethodMissingController get :shouldnt_be_called assert_response :success assert_equal 'shouldnt_be_called', @response.body end - + def test_method_missing_is_not_an_action_name use_controller MethodMissingController assert ! @controller.__send__(:action_method?, 'method_missing') - + get :method_missing assert_response :success assert_equal 'method_missing', @response.body end - + def test_get_on_hidden_should_fail use_controller NonEmptyController assert_raise(ActionController::UnknownAction) { get :hidden_action } diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index fe95fb5750..d6e2a5a974 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -390,7 +390,7 @@ class IntegrationProcessTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| set.draw do |map| - map.connect "/:action", :controller => "integration_process_test/integration" + match ':action', :to => IntegrationController end yield end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 054a9f2aaf..2b1f532b8d 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -343,9 +343,9 @@ class RescueTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| set.draw do |map| - map.connect 'foo', :controller => "rescue_test/test", :action => 'foo' - map.connect 'invalid', :controller => "rescue_test/test", :action => 'invalid' - map.connect 'b00m', :controller => "rescue_test/test", :action => 'b00m' + match 'foo', :to => TestController.action(:foo) + match 'invalid', :to => TestController.action(:invalid) + match 'b00m', :to => TestController.action(:b00m) end yield end diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index d81ced96a8..3b14cbb2d8 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -224,9 +224,8 @@ class UrlWriterTests < ActionController::TestCase def test_named_routes with_routing do |set| set.draw do |map| - map.no_args '/this/is/verbose', :controller => 'home', :action => 'index' - map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index' - map.connect ':controller/:action/:id' + match 'this/is/verbose', :to => 'home#index', :as => :no_args + match 'home/sweet/home/:user', :to => 'home#index', :as => :home end # We need to create a new class in order to install the new named route. @@ -264,7 +263,7 @@ class UrlWriterTests < ActionController::TestCase def test_only_path with_routing do |set| set.draw do |map| - map.home '/home/sweet/home/:user', :controller => 'home', :action => 'index' + match 'home/sweet/home/:user', :to => 'home#index', :as => :home map.connect ':controller/:action/:id' end @@ -334,7 +333,6 @@ class UrlWriterTests < ActionController::TestCase set.draw do |map| map.main '', :controller => 'posts', :format => nil map.resources :posts - map.connect ':controller/:action/:id' end # We need to create a new class in order to install the new named route. diff --git a/actionpack/test/controller/verification_test.rb b/actionpack/test/controller/verification_test.rb index 1a9eb65f29..63e8cf3e61 100644 --- a/actionpack/test/controller/verification_test.rb +++ b/actionpack/test/controller/verification_test.rb @@ -125,8 +125,8 @@ class VerificationTest < ActionController::TestCase assert_not_deprecated do with_routing do |set| set.draw do |map| - map.foo '/foo', :controller => 'test', :action => 'foo' - map.connect ":controller/:action/:id" + match 'foo', :to => 'test#foo', :as => :foo + match 'verification_test/:action', :to => TestController end get :guarded_one_for_named_route_test, :two => "not one" assert_redirected_to '/foo' diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index db6cf7b330..3c2408de5f 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -57,7 +57,7 @@ class JsonParamsParsingTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| set.draw do |map| - map.connect ':action', :controller => "json_params_parsing_test/test" + match ':action', :to => TestController end yield end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index a31e326ddf..b764478d87 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -109,7 +109,7 @@ class QueryStringParsingTest < ActionController::IntegrationTest def assert_parses(expected, actual) with_routing do |set| set.draw do |map| - map.connect ':action', :controller => "query_string_parsing_test/test" + match ':action', :to => TestController end get "/parse", actual diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 7167cdafac..e98a49980e 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -130,7 +130,7 @@ class UrlEncodedParamsParsingTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| set.draw do |map| - map.connect ':action', :controller => "url_encoded_params_parsing_test/test" + match ':action', :to => TestController end yield end diff --git a/actionpack/test/dispatch/request/xml_params_parsing_test.rb b/actionpack/test/dispatch/request/xml_params_parsing_test.rb index 521002b519..0dc47ed9d5 100644 --- a/actionpack/test/dispatch/request/xml_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/xml_params_parsing_test.rb @@ -84,7 +84,7 @@ class XmlParamsParsingTest < ActionController::IntegrationTest def with_test_routing with_routing do |set| set.draw do |map| - map.connect ':action', :controller => "xml_params_parsing_test/test" + match ':action', :to => TestController end yield end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index ab5fabde65..edfc303d3d 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -219,7 +219,7 @@ class CookieStoreTest < ActionController::IntegrationTest def with_test_route_set(options = {}) with_routing do |set| set.draw do |map| - map.connect "/:action", :controller => "cookie_store_test/test" + match ':action', :to => TestController end options = {:key => SessionKey, :secret => SessionSecret}.merge(options) @app = ActionDispatch::Session::CookieStore.new(set, options) diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index c7435bd06b..afc9d91d50 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -112,7 +112,7 @@ class MemCacheStoreTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| set.draw do |map| - map.connect "/:action", :controller => "mem_cache_store_test/test" + match ':action', :to => TestController end @app = ActionDispatch::Session::MemCacheStore.new(set, :key => '_session_id') yield diff --git a/actionpack/test/template/test_test.rb b/actionpack/test/template/test_test.rb index 05a14f3554..68e790cf46 100644 --- a/actionpack/test/template/test_test.rb +++ b/actionpack/test/template/test_test.rb @@ -48,8 +48,7 @@ class PeopleHelperTest < ActionView::TestCase def with_test_route_set with_routing do |set| set.draw do |map| - map.people 'people', :controller => 'people', :action => 'index' - map.connect ':controller/:action/:id' + match 'people', :to => 'people#index', :as => :people end yield end -- cgit v1.2.3 From 4f6d6f7031a88b647814fc0154e6b69b636dc912 Mon Sep 17 00:00:00 2001 From: Yehuda Katz + Carl Lerche Date: Tue, 20 Oct 2009 16:33:54 -0700 Subject: Have all the tests running off a single Gemfile --- actionpack/Gemfile | 18 ------------------ actionpack/test/abstract_unit.rb | 15 +++++++-------- 2 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 actionpack/Gemfile (limited to 'actionpack') diff --git a/actionpack/Gemfile b/actionpack/Gemfile deleted file mode 100644 index af95766608..0000000000 --- a/actionpack/Gemfile +++ /dev/null @@ -1,18 +0,0 @@ -rails_root = Pathname.new(File.dirname(__FILE__)).join("..") - -Gem.sources.each { |uri| source uri } - -gem "rack", "1.0.1", :git => "git://github.com/rails/rack.git", :branch => "rack-1.0" -gem "rack-mount", :git => "git://github.com/josh/rack-mount.git" -gem "rack-test", "~> 0.5.0" -gem "activesupport", "3.0.pre", :vendored_at => rails_root.join("activesupport") -gem "activemodel", "3.0.pre", :vendored_at => rails_root.join("activemodel") -gem "arel", :git => "git://github.com/rails/arel.git" -gem "activerecord", "3.0.pre", :vendored_at => rails_root.join("activerecord") -gem "erubis", "~> 2.6.0" - -gem "mocha" -gem "sqlite3-ruby" -gem "RedCloth" - -disable_system_gems diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index f7a1564f90..05b15d38ee 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,13 +1,12 @@ -bundled = "#{File.dirname(__FILE__)}/../vendor/gems/environment" -if File.exist?("#{bundled}.rb") - require bundled -else - $:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib" - $:.unshift "#{File.dirname(__FILE__)}/../../activemodel/lib" +root = File.expand_path('../../..', __FILE__) +begin + require "#{root}/vendor/gems/environment" +rescue LoadError + $:.unshift "#{root}/activesupport/lib" + $:.unshift "#{root}/activemodel/lib" + $:.unshift "#{root}/lib" end -$:.unshift(File.dirname(__FILE__) + '/../lib') - $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') -- cgit v1.2.3 From 9fbb2c571b65e0501bf3570a3d49e553a9ae39c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 21 Oct 2009 11:18:36 -0500 Subject: Fix error_messages_for when instance variable names are given. Signed-off-by: Joshua Peek --- actionpack/lib/action_view/helpers/active_model_helper.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 3c398fe4da..c70f29f098 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -191,13 +191,13 @@ module ActionView options = params.extract_options!.symbolize_keys objects = Array.wrap(options.delete(:object) || params).map do |object| - unless object.respond_to?(:to_model) - object = instance_variable_get("@#{object}") - object = convert_to_model(object) - else - object = object.to_model - options[:object_name] ||= object.class.model_name.human + object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model) + object = convert_to_model(object) + + if object.class.respond_to?(:model_name) + options[:object_name] ||= object.class.model_name.human.downcase end + object end -- cgit v1.2.3 From 3e35d30c0cab711fcc9feddd23f2f682f5d0a050 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 21 Oct 2009 11:32:31 -0500 Subject: Always add actionpack/lib to load path for isolated tests --- actionpack/test/abstract_unit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 05b15d38ee..86c8a95a43 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -4,9 +4,9 @@ begin rescue LoadError $:.unshift "#{root}/activesupport/lib" $:.unshift "#{root}/activemodel/lib" - $:.unshift "#{root}/lib" end +$:.unshift(File.dirname(__FILE__) + '/../lib') $:.unshift(File.dirname(__FILE__) + '/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') $:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers') -- cgit v1.2.3 From a840c8afbf4e30b7bd9979e8cd70192c65be7a43 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Sat, 24 Oct 2009 18:08:54 -0500 Subject: Restore `rake routes` [#3402 state:resolved] --- actionpack/lib/action_dispatch/routing.rb | 1 + .../action_dispatch/routing/deprecated_mapper.rb | 3 +- actionpack/lib/action_dispatch/routing/mapper.rb | 8 ++-- actionpack/lib/action_dispatch/routing/route.rb | 44 ++++++++++++++++++++++ .../lib/action_dispatch/routing/route_set.rb | 11 ++---- 5 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/route.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 3803929847..b9c377db2c 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -262,6 +262,7 @@ module ActionDispatch module Routing autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper' autoload :Mapper, 'action_dispatch/routing/mapper' + autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' SEPARATORS = %w( / . ? ) diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index f2a1f10fa7..0564ba9797 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -132,7 +132,6 @@ module ActionDispatch path = optionalize_trailing_dynamic_segments(path, requirements, defaults) glob = $1.to_sym if path =~ /\/\*(\w+)$/ path = ::Rack::Mount::Utils.normalize_path(path) - path = ::Rack::Mount::Strexp.compile(path, requirements, %w( / . ? )) if glob && !defaults[glob].blank? raise ActionController::RoutingError, "paths cannot have non-empty default values" @@ -145,7 +144,7 @@ module ActionDispatch conditions[:request_method] = method if method conditions[:path_info] = path if path - @set.add_route(app, conditions, defaults, name) + @set.add_route(app, conditions, requirements, defaults, name) end def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ab4193266a..d6d822842b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -252,9 +252,11 @@ module ActionDispatch constraints = (@scope[:constraints] || {}).merge(constraints) options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) } - conditions[:path_info] = Rack::Mount::Strexp.compile(path, constraints, %w( / . ? )) + conditions[:path_info] = path + requirements = constraints.dup - segment_keys = Rack::Mount::RegexpWithNamedGroups.new(conditions[:path_info]).names + path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS) + segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names constraints.reject! { |k, v| segment_keys.include?(k.to_s) } conditions.merge!(constraints) @@ -286,7 +288,7 @@ module ActionDispatch end app = Constraints.new(app, blocks) if blocks.any? - @set.add_route(app, conditions, defaults, options[:as]) + @set.add_route(app, conditions, requirements, defaults, options[:as]) self end diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb new file mode 100644 index 0000000000..8990e207c2 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -0,0 +1,44 @@ +module ActionDispatch + module Routing + class Route #:nodoc: + attr_reader :app, :conditions, :defaults, :name + attr_reader :path, :requirements + + def initialize(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + @app = app + @defaults = defaults + @name = name + + @requirements = requirements.merge(defaults) + @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) + @requirements.delete_if { |k, v| + v == Regexp.compile("[^#{SEPARATORS.join}]+") + } + + if path = conditions[:path_info] + @path = path + conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS) + end + + @conditions = conditions.inject({}) { |h, (k, v)| + h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) + h + } + end + + def verb + if verb = conditions[:verb] + verb.to_s.upcase + end + end + + def segment_keys + @segment_keys ||= conditions[:path_info].names.compact.map { |key| key.to_sym } + end + + def to_ary + [@app, @conditions, @defaults, @name] + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 90d7c208a5..93617e826d 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -47,11 +47,6 @@ module ActionDispatch end end - module RouteExtensions - def segment_keys - conditions[:path_info].names.compact.map { |key| key.to_sym } - end - end # A NamedRouteCollection instance is a collection of named routes, and also # maintains an anonymous module that can be used to install helpers for the @@ -290,9 +285,9 @@ module ActionDispatch routes_changed_at end - def add_route(app, conditions = {}, defaults = {}, name = nil) - route = @set.add_route(app, conditions, defaults, name) - route.extend(RouteExtensions) + def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + route = Route.new(app, conditions, requirements, defaults, name) + @set.add_route(*route) named_routes[name] = route if name routes << route route -- cgit v1.2.3 From 6083a87d63af9ca1b66fedcb92cd4a395965173b Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 26 Oct 2009 11:16:17 -0500 Subject: Splat calls to_a not to_ary [#3423 state:resolved] --- actionpack/lib/action_dispatch/routing/route.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index 8990e207c2..e3aaa29e76 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -36,7 +36,7 @@ module ActionDispatch @segment_keys ||= conditions[:path_info].names.compact.map { |key| key.to_sym } end - def to_ary + def to_a [@app, @conditions, @defaults, @name] end end -- cgit v1.2.3 From 55ae53baadf37daf2c966bf5d9d67c1f954cb681 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 26 Oct 2009 11:23:39 -0500 Subject: Fix `rake routes` method name output [#3422 state:resolved] --- actionpack/lib/action_dispatch/routing/route.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index e3aaa29e76..f1431e7a37 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -27,8 +27,13 @@ module ActionDispatch end def verb - if verb = conditions[:verb] - verb.to_s.upcase + if method = conditions[:request_method] + case method + when Regexp + method.source.upcase + else + method.to_s.upcase + end end end -- cgit v1.2.3 From 60cc86136bffc96bc62798538e08e0d28138d41c Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 15:47:08 -0700 Subject: Base setup for config object in AC. --- actionpack/lib/action_controller.rb | 1 + actionpack/lib/action_controller/base.rb | 1 + .../lib/action_controller/metal/configuration.rb | 28 ++++++++++++++++++++++ .../action_controller/metal/session_management.rb | 15 +++--------- 4 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 actionpack/lib/action_controller/metal/configuration.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 9db1a71202..c5de4361bb 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -2,6 +2,7 @@ module ActionController autoload :Base, "action_controller/base" autoload :Benchmarking, "action_controller/metal/benchmarking" autoload :ConditionalGet, "action_controller/metal/conditional_get" + autoload :Configuration, "action_controller/metal/configuration" autoload :Helpers, "action_controller/metal/helpers" autoload :HideActions, "action_controller/metal/hide_actions" autoload :Layouts, "action_controller/metal/layouts" diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5338a70104..4c026fe5f7 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -15,6 +15,7 @@ module ActionController include ActionController::ConditionalGet include ActionController::RackConvenience include ActionController::Benchmarking + include ActionController::Configuration # Legacy modules include SessionManagement diff --git a/actionpack/lib/action_controller/metal/configuration.rb b/actionpack/lib/action_controller/metal/configuration.rb new file mode 100644 index 0000000000..5c829853b7 --- /dev/null +++ b/actionpack/lib/action_controller/metal/configuration.rb @@ -0,0 +1,28 @@ +module ActionController + module Configuration + extend ActiveSupport::Concern + + def config + @config ||= self.class.config + end + + def config=(config) + @config = config + end + + module ClassMethods + def default_config + @default_config ||= {} + end + + def config + self.config ||= default_config + end + + def config=(config) + @config = ActiveSupport::OrderedHash.new + @config.merge!(config) + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/metal/session_management.rb b/actionpack/lib/action_controller/metal/session_management.rb index 654aa08cd3..9f4c4b8b39 100644 --- a/actionpack/lib/action_controller/metal/session_management.rb +++ b/actionpack/lib/action_controller/metal/session_management.rb @@ -1,10 +1,8 @@ module ActionController #:nodoc: module SessionManagement #:nodoc: - def self.included(base) - base.class_eval do - extend ClassMethods - end - end + include ActiveSupport::Concern + + include ActionController::Configuration module ClassMethods # Set the session store to be used for keeping the session data between requests. @@ -35,13 +33,6 @@ module ActionController #:nodoc: session_options.merge!(options) end - # Returns the hash used to configure the session. Example use: - # - # ActionController::Base.session_options[:secure] = true # session only available over HTTPS - def session_options - @session_options ||= {} - end - def session(*args) ActiveSupport::Deprecation.warn( "Disabling sessions for a single controller has been deprecated. " + -- cgit v1.2.3 From d7499f8ee8faa80d12dccae5baf5ab2acc79e77d Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 17:13:43 -0700 Subject: Extract #head into its own module and simplify it --- actionpack/lib/action_controller.rb | 1 + .../lib/action_controller/metal/conditional_get.rb | 39 ++-------------------- actionpack/lib/action_controller/metal/head.rb | 27 +++++++++++++++ 3 files changed, 31 insertions(+), 36 deletions(-) create mode 100644 actionpack/lib/action_controller/metal/head.rb (limited to 'actionpack') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index c5de4361bb..03a40e4fce 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -3,6 +3,7 @@ module ActionController autoload :Benchmarking, "action_controller/metal/benchmarking" autoload :ConditionalGet, "action_controller/metal/conditional_get" autoload :Configuration, "action_controller/metal/configuration" + autoload :Head, "action_controller/metal/head" autoload :Helpers, "action_controller/metal/helpers" autoload :HideActions, "action_controller/metal/hide_actions" autoload :Layouts, "action_controller/metal/layouts" diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 8575d30335..52f5a9727e 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -3,6 +3,7 @@ module ActionController extend ActiveSupport::Concern include RackConvenience + include Head # Sets the etag, last_modified, or both on the response and renders a # "304 Not Modified" response if the request is already fresh. @@ -27,43 +28,9 @@ module ActionController response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] + response.cache_control[:public] = true if options[:public] - if options[:public] - response.cache_control[:public] = true - end - - if request.fresh?(response) - head :not_modified - end - end - - # Return a response that has no content (merely headers). The options - # argument is interpreted to be a hash of header names and values. - # This allows you to easily return a response that consists only of - # significant headers: - # - # head :created, :location => person_path(@person) - # - # It can also be used to return exceptional conditions: - # - # return head(:method_not_allowed) unless request.post? - # return head(:bad_request) unless valid_request? - # render - def head(*args) - if args.length > 2 - raise ArgumentError, "too many arguments to head" - elsif args.empty? - raise ArgumentError, "too few arguments to head" - end - options = args.extract_options! - status = args.shift || options.delete(:status) || :ok - location = options.delete(:location) - - options.each do |key, value| - headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s - end - - render :nothing => true, :status => status, :location => location + head :not_modified if request.fresh?(response) end # Sets the etag and/or last_modified on the response and checks it against diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb new file mode 100644 index 0000000000..68fa0a0402 --- /dev/null +++ b/actionpack/lib/action_controller/metal/head.rb @@ -0,0 +1,27 @@ +module ActionController + module Head + # Return a response that has no content (merely headers). The options + # argument is interpreted to be a hash of header names and values. + # This allows you to easily return a response that consists only of + # significant headers: + # + # head :created, :location => person_path(@person) + # + # It can also be used to return exceptional conditions: + # + # return head(:method_not_allowed) unless request.post? + # return head(:bad_request) unless valid_request? + # render + def head(status, options = {}) + options, status = status, nil if status.is_a?(Hash) + status ||= options.delete(:status) || :ok + location = options.delete(:location) + + options.each do |key, value| + headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + end + + render :nothing => true, :status => status, :location => location + end + end +end \ No newline at end of file -- cgit v1.2.3 From e1786ee6ebee9fab10d6756be1eeacbbe6b65b48 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 17:32:42 -0700 Subject: Fixes expires_now and cleans things up a bit --- .../lib/action_controller/metal/conditional_get.rb | 2 +- actionpack/lib/action_dispatch/http/request.rb | 21 +++++++++------------ actionpack/lib/action_dispatch/http/response.rb | 2 ++ actionpack/test/controller/render_test.rb | 10 ++++++++++ 4 files changed, 22 insertions(+), 13 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 52f5a9727e..5156fbc1d5 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -80,7 +80,7 @@ module ActionController # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or # intermediate caches (like caching proxy servers). def expires_now #:doc: - response.headers["Cache-Control"] = "no-cache" + response.cache_control.replace(:no_cache => true) end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index bff030f0e4..1e366520c9 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -136,19 +136,16 @@ module ActionDispatch # If-Modified-Since and If-None-Match conditions. If both headers are # supplied, both must match, or the request is not considered fresh. def fresh?(response) - case - when if_modified_since && if_none_match - not_modified?(response.last_modified) && etag_matches?(response.etag) - when if_modified_since - not_modified?(response.last_modified) - when if_none_match - etag_matches?(response.etag) - else - false - end - end + last_modified = if_modified_since + etag = if_none_match - ONLY_ALL = [Mime::ALL].freeze + return false unless last_modified || etag + + success = true + success &&= not_modified?(response.last_modified) if last_modified + success &&= etag_matches?(response.etag) if etag + success + end # Returns the Mime type for the \format used in the request. # diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3e3b473178..b3ed7c9d1a 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -270,6 +270,8 @@ module ActionDispatch # :nodoc: if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + elsif @cache_control[:no_cache] + headers["Cache-Control"] = "no-cache" else extras = control[:extras] max_age = control[:max_age] diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 2db524ca4b..ac8dad7c42 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -73,6 +73,11 @@ class TestController < ActionController::Base render :action => 'hello_world' end + def conditional_hello_with_expires_now + expires_now + render :action => 'hello_world' + end + def conditional_hello_with_bangs render :action => 'hello_world' end @@ -1321,6 +1326,11 @@ class ExpiresInRenderTest < ActionController::TestCase get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"] end + + def test_expires_now + get :conditional_hello_with_expires_now + assert_equal "no-cache", @response.headers["Cache-Control"] + end end -- cgit v1.2.3 From 000d5936216f363a5b11013f664959019b7ebac2 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 18:01:09 -0700 Subject: Clean up and update cookies --- actionpack/lib/action_controller/metal/cookies.rb | 44 ++++++++++++++--------- actionpack/test/controller/cookie_test.rb | 11 +++--- 2 files changed, 32 insertions(+), 23 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index c328db8beb..6855ca1478 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -44,24 +44,31 @@ module ActionController #:nodoc: # * :httponly - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. module Cookies - def self.included(base) - base.helper_method :cookies + extend ActiveSupport::Concern + + include RackConvenience + + included do + helper_method :cookies end - protected - # Returns the cookie container, which operates as described above. - def cookies - @cookies ||= CookieJar.new(self) - end + protected + # Returns the cookie container, which operates as described above. + def cookies + @cookies ||= CookieJar.build(request, response) + end end class CookieJar < Hash #:nodoc: - def initialize(controller) - @controller, @cookies = controller, controller.request.cookies - super() - update(@cookies) + def self.build(request, response) + new.tap do |hash| + hash.update(request.cookies) + hash.response = response + end end + attr_accessor :response + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) super(name.to_s) @@ -72,13 +79,16 @@ module ActionController #:nodoc: def []=(key, options) if options.is_a?(Hash) options.symbolize_keys! + value = options[:value] else - options = { :value => options } + value = options + options = { :value => value } end - options[:path] = "/" unless options.has_key?(:path) - super(key.to_s, options[:value]) - @controller.response.set_cookie(key, options) + super(key.to_s, value) + + options[:path] ||= "/" + response.set_cookie(key, options) end # Removes the cookie on the client machine by setting the value to an empty string @@ -86,9 +96,9 @@ module ActionController #:nodoc: # an options hash to delete cookies with extra data such as a :path. def delete(key, options = {}) options.symbolize_keys! - options[:path] = "/" unless options.has_key?(:path) + options[:path] ||= "/" value = super(key.to_s) - @controller.response.delete_cookie(key, options) + response.delete_cookie(key, options) value end end diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb index b429cbf0e6..53d4364576 100644 --- a/actionpack/test/controller/cookie_test.rb +++ b/actionpack/test/controller/cookie_test.rb @@ -106,7 +106,7 @@ class CookieTest < ActionController::TestCase def test_cookiejar_accessor @request.cookies["user_name"] = "david" @controller.request = @request - jar = ActionController::CookieJar.new(@controller) + jar = ActionController::CookieJar.build(@controller.request, @controller.response) assert_equal "david", jar["user_name"] assert_equal nil, jar["something_else"] end @@ -114,14 +114,14 @@ class CookieTest < ActionController::TestCase def test_cookiejar_accessor_with_array_value @request.cookies["pages"] = %w{1 2 3} @controller.request = @request - jar = ActionController::CookieJar.new(@controller) + jar = ActionController::CookieJar.build(@controller.request, @controller.response) assert_equal %w{1 2 3}, jar["pages"] end def test_cookiejar_delete_removes_item_and_returns_its_value @request.cookies["user_name"] = "david" @controller.response = @response - jar = ActionController::CookieJar.new(@controller) + jar = ActionController::CookieJar.build(@controller.request, @controller.response) assert_equal "david", jar.delete("user_name") end @@ -131,9 +131,8 @@ class CookieTest < ActionController::TestCase end def test_cookies_persist_throughout_request - get :authenticate - cookies = @controller.send(:cookies) - assert_equal 'david', cookies['user_name'] + response = get :authenticate + assert response.headers["Set-Cookie"] =~ /user_name=david/ end private -- cgit v1.2.3 From 2bdd8fa86313a48de11d95fc48f97ada24d7d8af Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 21:10:40 -0700 Subject: Clean up parameter logging some --- .../metal/filter_parameter_logging.rb | 65 ++++++++-------------- actionpack/test/controller/filter_params_test.rb | 18 +++--- 2 files changed, 32 insertions(+), 51 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/filter_parameter_logging.rb b/actionpack/lib/action_controller/metal/filter_parameter_logging.rb index 4259d9de19..a53c052075 100644 --- a/actionpack/lib/action_controller/metal/filter_parameter_logging.rb +++ b/actionpack/lib/action_controller/metal/filter_parameter_logging.rb @@ -4,10 +4,6 @@ module ActionController include AbstractController::Logger - included do - include InstanceMethodsForNewBase - end - module ClassMethods # Replace sensitive parameter data from the request log. # Filters parameters that have any of the arguments as a substring. @@ -17,8 +13,6 @@ module ActionController # can be replaced using String#replace or similar method. # # Examples: - # filter_parameter_logging - # => Does nothing, just slows the logging process down # # filter_parameter_logging :password # => replaces the value to all keys matching /password/i with "[FILTERED]" @@ -33,64 +27,51 @@ module ActionController # => reverses the value to all keys matching /secret/i, and # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" def filter_parameter_logging(*filter_words, &block) - parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 + raise "You must filter at least one word from logging" if filter_words.empty? + + parameter_filter = Regexp.new(filter_words.join('|'), true) - define_method(:filter_parameters) do |unfiltered_parameters| - filtered_parameters = {} + define_method(:filter_parameters) do |original_params| + filtered_params = {} - unfiltered_parameters.each do |key, value| + original_params.each do |key, value| if key =~ parameter_filter - filtered_parameters[key] = '[FILTERED]' + value = '[FILTERED]' elsif value.is_a?(Hash) - filtered_parameters[key] = filter_parameters(value) + value = filter_parameters(value) elsif value.is_a?(Array) - filtered_parameters[key] = value.collect do |item| - filter_parameters(item) - end + value = value.map { |item| filter_parameters(item) } elsif block_given? key = key.dup value = value.dup if value.duplicable? yield key, value - filtered_parameters[key] = value - else - filtered_parameters[key] = value end + + filtered_params[key] = value end - filtered_parameters + filtered_params end protected :filter_parameters end end - module InstanceMethodsForNewBase - # TODO : Fix the order of information inside such that it's exactly same as the old base - def process(*) - ret = super - - if logger - parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup - parameters = parameters.except!(:controller, :action, :format, :_method, :only_path) + INTERNAL_PARAMS = [:controller, :action, :format, :_method, :only_path] - unless parameters.empty? - # TODO : Move DelayedLog to AS - log = AbstractController::Logger::DelayedLog.new { " Parameters: #{parameters.inspect}" } - logger.info(log) - end - end - - ret + def process(*) + response = super + if logger + parameters = filter_parameters(params).except!(*INTERNAL_PARAMS) + logger.info { " Parameters: #{parameters.inspect}" } unless parameters.empty? end + response end - private + protected - # TODO : This method is not needed for the new base - def log_processing_for_parameters - parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup - parameters = parameters.except!(:controller, :action, :format, :_method) - - logger.info " Parameters: #{parameters.inspect}" unless parameters.empty? + def filter_parameters(params) + params.dup end + end end diff --git a/actionpack/test/controller/filter_params_test.rb b/actionpack/test/controller/filter_params_test.rb index 19232c6bc9..43bef34885 100644 --- a/actionpack/test/controller/filter_params_test.rb +++ b/actionpack/test/controller/filter_params_test.rb @@ -19,23 +19,23 @@ class FilterParamTest < ActionController::TestCase def method_missing(method, *args) @logged ||= [] - @logged << args.first + @logged << args.first unless block_given? + @logged << yield if block_given? end end setup :set_logger + def test_filter_parameters_must_have_one_word + assert_raises RuntimeError do + FilterParamController.filter_parameter_logging + end + end + def test_filter_parameters assert FilterParamController.respond_to?(:filter_parameter_logging) - assert !@controller.respond_to?(:filter_parameters) - - FilterParamController.filter_parameter_logging - assert @controller.respond_to?(:filter_parameters) - test_hashes = [[{},{},[]], - [{'foo'=>nil},{'foo'=>nil},[]], - [{'foo'=>'bar'},{'foo'=>'bar'},[]], - [{'foo'=>1},{'foo'=>1},[]], + test_hashes = [ [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], [{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'], [{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'], -- cgit v1.2.3 From df06e0bd86bc054fc4e3d78193c09da9e27971a4 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 23:11:52 -0700 Subject: Clean up flash a bit --- actionpack/lib/action_controller/metal/flash.rb | 48 ++++++++++++------------- 1 file changed, 23 insertions(+), 25 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 590f9be3ac..b2fc359df4 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -49,7 +49,7 @@ module ActionController #:nodoc: class FlashHash < Hash def initialize #:nodoc: super - @used = {} + @used = Set.new end def []=(k, v) #:nodoc: @@ -65,7 +65,7 @@ module ActionController #:nodoc: alias :merge! :update def replace(h) #:nodoc: - @used = {} + @used = Set.new super end @@ -104,8 +104,8 @@ module ActionController #:nodoc: # This method is called automatically by filters, so you generally don't need to care about it. def sweep #:nodoc: keys.each do |k| - unless @used[k] - use(k) + unless @used.include?(k) + @used << k else delete(k) @used.delete(k) @@ -113,47 +113,45 @@ module ActionController #:nodoc: end # clean up after keys that could have been left over by calling reject! or shift on the flash - (@used.keys - keys).each{ |k| @used.delete(k) } + (@used - keys).each{ |k| @used.delete(k) } end - def store(session, key = "flash") + def store(session) return if self.empty? - session[key] = self + session["flash"] = self end - private - # Used internally by the keep and discard methods - # use() # marks the entire flash as used - # use('msg') # marks the "msg" entry as used - # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) - # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself - # if no key is passed. - def use(key = nil, used = true) - Array(key || keys).each { |k| @used[k] = used } - return key ? self[key] : self - end + private + # Used internally by the keep and discard methods + # use() # marks the entire flash as used + # use('msg') # marks the "msg" entry as used + # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) + # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) + # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself + # if no key is passed. + def use(key = nil, used = true) + Array(key || keys).each { |k| used ? @used << k : @used.delete(k) } + return key ? self[key] : self + end end protected def process_action(method_name) super - if defined? @_flash - @_flash.store(session) - remove_instance_variable(:@_flash) - end + @_flash.store(session) if @_flash + @_flash = nil end def reset_session super - remove_instance_variable(:@_flash) if defined?(@_flash) + @_flash = nil end # Access the contents of the flash. Use flash["notice"] to # read a notice you put there or flash["notice"] = "hello" # to put a new one. def flash #:doc: - if !defined?(@_flash) + if !@_flash @_flash = session["flash"] || FlashHash.new @_flash.sweep end -- cgit v1.2.3 From 4653719aa6407f7d0288210204c7f3d8f2728489 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Mon, 26 Oct 2009 23:11:52 -0700 Subject: Clean up flash a bit --- actionpack/lib/action_controller/metal/flash.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index b2fc359df4..f43900faa0 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -151,7 +151,7 @@ module ActionController #:nodoc: # read a notice you put there or flash["notice"] = "hello" # to put a new one. def flash #:doc: - if !@_flash + unless @_flash @_flash = session["flash"] || FlashHash.new @_flash.sweep end -- cgit v1.2.3 From c3fa20883e667ce2e7ce207ce8f08ba64f7dcf23 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Tue, 27 Oct 2009 09:21:01 -0700 Subject: #include should be #extend --- actionpack/lib/action_controller/metal/session_management.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/session_management.rb b/actionpack/lib/action_controller/metal/session_management.rb index 9f4c4b8b39..d70f40ce7a 100644 --- a/actionpack/lib/action_controller/metal/session_management.rb +++ b/actionpack/lib/action_controller/metal/session_management.rb @@ -1,6 +1,6 @@ module ActionController #:nodoc: module SessionManagement #:nodoc: - include ActiveSupport::Concern + extend ActiveSupport::Concern include ActionController::Configuration -- cgit v1.2.3 From 759f2ccc4cebd28534aee9511af8ba3a33e33a80 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 27 Oct 2009 18:26:53 -0500 Subject: Start migrating AC routing tests into dispatch folder --- actionpack/test/controller/routing_test.rb | 332 ---------------------------- actionpack/test/dispatch/routing_test.rb | 336 +++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 332 deletions(-) create mode 100644 actionpack/test/dispatch/routing_test.rb (limited to 'actionpack') diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index d7e4646df5..308e2a85b1 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -2046,335 +2046,3 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert true end end - -class TestRoutingMapper < ActiveSupport::TestCase - include Rack::Test::Methods - - SprocketsApp = lambda { |env| - [200, {"Content-Type" => "text/html"}, ["javascripts"]] - } - - class IpRestrictor - def self.matches?(request) - request.ip =~ /192\.168\.1\.1\d\d/ - end - end - - class Dispatcher - def self.new(*args) - lambda { |env| - params = env['action_dispatch.request.path_parameters'] - controller, action = params[:controller], params[:action] - [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] - } - end - end - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, Dispatcher } - - Routes = ActionDispatch::Routing::RouteSet.new - Routes.draw do - controller :sessions do - get 'login', :to => :new, :as => :login - post 'login', :to => :create - - delete 'logout', :to => :destroy, :as => :logout - end - - match 'account/login', :to => redirect("/login") - - match 'openid/login', :via => [:get, :post], :to => "openid#login" - - controller(:global) do - match 'global/:action' - match 'global/export', :to => :export, :as => :export_request - match 'global/hide_notice', :to => :hide_notice, :as => :hide_notice - match '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } - end - - constraints(:ip => /192\.168\.1\.\d\d\d/) do - get 'admin', :to => "queenbee#index" - end - - constraints IpRestrictor do - get 'admin/accounts', :to => "queenbee#accounts" - end - - resources :projects, :controller => :project do - resources :involvements, :attachments - - resources :participants do - put :update_all, :on => :collection - end - - resources :companies do - resources :people - resource :avatar - end - - resources :images do - post :revise, :on => :member - end - - resources :people do - namespace ":access_token" do - resource :avatar - end - - member do - put :accessible_projects - post :resend, :generate_new_password - end - end - - resources :posts do - get :archive, :toggle_view, :on => :collection - post :preview, :on => :member - - resource :subscription - - resources :comments do - post :preview, :on => :collection - end - end - end - - match 'sprockets.js', :to => SprocketsApp - - match 'people/:id/update', :to => 'people#update', :as => :update_person - match '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person - - # misc - match 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article - - namespace :account do - resource :subscription, :credit, :credit_card - end - - controller :articles do - scope 'articles' do - scope ':title', :title => /[a-z]+/, :as => :with_title do - match ':id', :to => :with_id - end - end - end - - scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do - resources :rooms - end - end - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } - - def app - Routes - end - - def test_logout - delete '/logout' - assert_equal 'sessions#destroy', last_response.body - - # assert_equal '/logout', app.logout_path - end - - def test_login - get '/login' - assert_equal 'sessions#new', last_response.body - - post '/login' - assert_equal 'sessions#create', last_response.body - - # assert_equal '/login', app.login_path - end - - def test_login_redirect - get '/account/login' - assert_equal 301, last_response.status - assert_equal 'http://example.org/login', last_response.headers['Location'] - assert_equal 'Moved Permanently', last_response.body - end - - def test_openid - get '/openid/login' - assert_equal 'openid#login', last_response.body - - post '/openid/login' - assert_equal 'openid#login', last_response.body - end - - # def test_admin - # get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} - # assert_equal 'queenbee#index', last_response.body - # - # assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } - # - # get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} - # assert_equal 'queenbee#accounts', last_response.body - # - # assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } - # end - - def test_global - get '/global/dashboard' - assert_equal 'global#dashboard', last_response.body - - get '/global/export' - assert_equal 'global#export', last_response.body - - get '/global/hide_notice' - assert_equal 'global#hide_notice', last_response.body - - get '/export/123/foo.txt' - assert_equal 'global#export', last_response.body - - # assert_equal '/global/export', app.export_request_path - # assert_equal '/global/hide_notice', app.hide_notice_path - # assert_equal '/export/123/foo.txt', app.export_download_path(:id => 123, :file => 'foo.txt') - end - - def test_projects - get '/projects/1' - assert_equal 'projects#show', last_response.body - end - - def test_projects_involvements - get '/projects/1/involvements' - assert_equal 'involvements#index', last_response.body - - get '/projects/1/involvements/1' - assert_equal 'involvements#show', last_response.body - end - - def test_projects_attachments - get '/projects/1/attachments' - assert_equal 'attachments#index', last_response.body - end - - def test_projects_participants - get '/projects/1/participants' - assert_equal 'participants#index', last_response.body - - put '/projects/1/participants/update_all' - assert_equal 'participants#update_all', last_response.body - end - - def test_projects_companies - get '/projects/1/companies' - assert_equal 'companies#index', last_response.body - - get '/projects/1/companies/1/people' - assert_equal 'people#index', last_response.body - - get '/projects/1/companies/1/avatar' - assert_equal 'avatar#show', last_response.body - end - - def test_project_images - get '/projects/1/images' - assert_equal 'images#index', last_response.body - - post '/projects/1/images/1/revise' - assert_equal 'images#revise', last_response.body - end - - def test_projects_people - get '/projects/1/people' - assert_equal 'people#index', last_response.body - - get '/projects/1/people/1' - assert_equal 'people#show', last_response.body - - get '/projects/1/people/1/7a2dec8/avatar' - assert_equal 'avatar#show', last_response.body - - put '/projects/1/people/1/accessible_projects' - assert_equal 'people#accessible_projects', last_response.body - - post '/projects/1/people/1/resend' - assert_equal 'people#resend', last_response.body - - post '/projects/1/people/1/generate_new_password' - assert_equal 'people#generate_new_password', last_response.body - end - - def test_projects_posts - get '/projects/1/posts' - assert_equal 'posts#index', last_response.body - - get '/projects/1/posts/archive' - assert_equal 'posts#archive', last_response.body - - get '/projects/1/posts/toggle_view' - assert_equal 'posts#toggle_view', last_response.body - - post '/projects/1/posts/1/preview' - assert_equal 'posts#preview', last_response.body - - get '/projects/1/posts/1/subscription' - assert_equal 'subscription#show', last_response.body - - get '/projects/1/posts/1/comments' - assert_equal 'comments#index', last_response.body - - post '/projects/1/posts/1/comments/preview' - assert_equal 'comments#preview', last_response.body - end - - def test_sprockets - get '/sprockets.js' - assert_equal 'javascripts', last_response.body - end - - def test_update_person_route - get '/people/1/update' - assert_equal 'people#update', last_response.body - - # assert_equal '/people/1/update', app.update_person_path(:id => 1) - end - - def test_update_project_person - get '/projects/1/people/2/update' - assert_equal 'people#update', last_response.body - - # assert_equal '/projects/1/people/2/update', app.update_project_person_path(:project_id => 1, :id => 2) - end - - def test_articles_perma - get '/articles/2009/08/18/rails-3' - assert_equal 'articles#show', last_response.body - - # assert_equal '/articles/2009/8/18/rails-3', app.article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') - end - - def test_account_namespace - get '/account/subscription' - assert_equal 'subscription#show', last_response.body - - get '/account/credit' - assert_equal 'credit#show', last_response.body - - get '/account/credit_card' - assert_equal 'credit_card#show', last_response.body - end - - def test_articles_with_id - get '/articles/rails/1' - assert_equal 'articles#with_id', last_response.body - - assert_raise(ActionController::RoutingError) { get '/articles/123/1' } - - # assert_equal '/articles/rails/1', app.with_title_path(:title => 'rails', :id => 1) - end - - def test_access_token_rooms - get '/12345/rooms' - assert_equal 'rooms#index', last_response.body - - get '/12345/rooms/1' - assert_equal 'rooms#show', last_response.body - - get '/12345/rooms/1/edit' - assert_equal 'rooms#edit', last_response.body - end -end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb new file mode 100644 index 0000000000..ea7113a602 --- /dev/null +++ b/actionpack/test/dispatch/routing_test.rb @@ -0,0 +1,336 @@ +require 'abstract_unit' +require 'controller/fake_controllers' + +class TestRoutingMapper < ActionDispatch::IntegrationTest + SprocketsApp = lambda { |env| + [200, {"Content-Type" => "text/html"}, ["javascripts"]] + } + + class IpRestrictor + def self.matches?(request) + request.ip =~ /192\.168\.1\.1\d\d/ + end + end + + class Dispatcher + def self.new(*args) + lambda { |env| + params = env['action_dispatch.request.path_parameters'] + controller, action = params[:controller], params[:action] + [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] + } + end + end + old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, Dispatcher } + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + controller :sessions do + get 'login', :to => :new, :as => :login + post 'login', :to => :create + + delete 'logout', :to => :destroy, :as => :logout + end + + match 'account/login', :to => redirect("/login") + + match 'openid/login', :via => [:get, :post], :to => "openid#login" + + controller(:global) do + match 'global/:action' + match 'global/export', :to => :export, :as => :export_request + match 'global/hide_notice', :to => :hide_notice, :as => :hide_notice + match '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } + end + + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin', :to => "queenbee#index" + end + + constraints IpRestrictor do + get 'admin/accounts', :to => "queenbee#accounts" + end + + resources :projects, :controller => :project do + resources :involvements, :attachments + + resources :participants do + put :update_all, :on => :collection + end + + resources :companies do + resources :people + resource :avatar + end + + resources :images do + post :revise, :on => :member + end + + resources :people do + namespace ":access_token" do + resource :avatar + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end + end + + resources :posts do + get :archive, :toggle_view, :on => :collection + post :preview, :on => :member + + resource :subscription + + resources :comments do + post :preview, :on => :collection + end + end + end + + match 'sprockets.js', :to => SprocketsApp + + match 'people/:id/update', :to => 'people#update', :as => :update_person + match '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person + + # misc + match 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article + + namespace :account do + resource :subscription, :credit, :credit_card + end + + controller :articles do + scope 'articles' do + scope ':title', :title => /[a-z]+/, :as => :with_title do + match ':id', :to => :with_id + end + end + end + + scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do + resources :rooms + end + end + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + + def app + Routes + end + + def setup + Routes.install_helpers(metaclass) + end + + def test_logout + delete '/logout' + assert_equal 'sessions#destroy', @response.body + + # assert_equal '/logout', logout_path + end + + def test_login + get '/login' + assert_equal 'sessions#new', @response.body + + post '/login' + assert_equal 'sessions#create', @response.body + + # assert_equal '/login', app.login_path + end + + def test_login_redirect + get '/account/login' + assert_equal 301, @response.status + assert_equal 'http://www.example.com/login', @response.headers['Location'] + assert_equal 'Moved Permanently', @response.body + end + + def test_openid + get '/openid/login' + assert_equal 'openid#login', @response.body + + post '/openid/login' + assert_equal 'openid#login', @response.body + end + + # def test_admin + # get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#index', @response.body + # + # assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # + # get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#accounts', @response.body + # + # assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # end + + def test_global + get '/global/dashboard' + assert_equal 'global#dashboard', @response.body + + get '/global/export' + assert_equal 'global#export', @response.body + + get '/global/hide_notice' + assert_equal 'global#hide_notice', @response.body + + get '/export/123/foo.txt' + assert_equal 'global#export', @response.body + + # assert_equal '/global/export', app.export_request_path + # assert_equal '/global/hide_notice', app.hide_notice_path + # assert_equal '/export/123/foo.txt', app.export_download_path(:id => 123, :file => 'foo.txt') + end + + def test_projects + get '/projects/1' + assert_equal 'projects#show', @response.body + end + + def test_projects_involvements + get '/projects/1/involvements' + assert_equal 'involvements#index', @response.body + + get '/projects/1/involvements/1' + assert_equal 'involvements#show', @response.body + end + + def test_projects_attachments + get '/projects/1/attachments' + assert_equal 'attachments#index', @response.body + end + + def test_projects_participants + get '/projects/1/participants' + assert_equal 'participants#index', @response.body + + put '/projects/1/participants/update_all' + assert_equal 'participants#update_all', @response.body + end + + def test_projects_companies + get '/projects/1/companies' + assert_equal 'companies#index', @response.body + + get '/projects/1/companies/1/people' + assert_equal 'people#index', @response.body + + get '/projects/1/companies/1/avatar' + assert_equal 'avatar#show', @response.body + end + + def test_project_images + get '/projects/1/images' + assert_equal 'images#index', @response.body + + post '/projects/1/images/1/revise' + assert_equal 'images#revise', @response.body + end + + def test_projects_people + get '/projects/1/people' + assert_equal 'people#index', @response.body + + get '/projects/1/people/1' + assert_equal 'people#show', @response.body + + get '/projects/1/people/1/7a2dec8/avatar' + assert_equal 'avatar#show', @response.body + + put '/projects/1/people/1/accessible_projects' + assert_equal 'people#accessible_projects', @response.body + + post '/projects/1/people/1/resend' + assert_equal 'people#resend', @response.body + + post '/projects/1/people/1/generate_new_password' + assert_equal 'people#generate_new_password', @response.body + end + + def test_projects_posts + get '/projects/1/posts' + assert_equal 'posts#index', @response.body + + get '/projects/1/posts/archive' + assert_equal 'posts#archive', @response.body + + get '/projects/1/posts/toggle_view' + assert_equal 'posts#toggle_view', @response.body + + post '/projects/1/posts/1/preview' + assert_equal 'posts#preview', @response.body + + get '/projects/1/posts/1/subscription' + assert_equal 'subscription#show', @response.body + + get '/projects/1/posts/1/comments' + assert_equal 'comments#index', @response.body + + post '/projects/1/posts/1/comments/preview' + assert_equal 'comments#preview', @response.body + end + + def test_sprockets + get '/sprockets.js' + assert_equal 'javascripts', @response.body + end + + def test_update_person_route + get '/people/1/update' + assert_equal 'people#update', @response.body + + # assert_equal '/people/1/update', app.update_person_path(:id => 1) + end + + def test_update_project_person + get '/projects/1/people/2/update' + assert_equal 'people#update', @response.body + + # assert_equal '/projects/1/people/2/update', app.update_project_person_path(:project_id => 1, :id => 2) + end + + def test_articles_perma + get '/articles/2009/08/18/rails-3' + assert_equal 'articles#show', @response.body + + # assert_equal '/articles/2009/8/18/rails-3', app.article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') + end + + def test_account_namespace + get '/account/subscription' + assert_equal 'subscription#show', @response.body + + get '/account/credit' + assert_equal 'credit#show', @response.body + + get '/account/credit_card' + assert_equal 'credit_card#show', @response.body + end + + def test_articles_with_id + get '/articles/rails/1' + assert_equal 'articles#with_id', @response.body + + assert_raise(ActionController::RoutingError) { get '/articles/123/1' } + + # assert_equal '/articles/rails/1', app.with_title_path(:title => 'rails', :id => 1) + end + + def test_access_token_rooms + get '/12345/rooms' + assert_equal 'rooms#index', @response.body + + get '/12345/rooms/1' + assert_equal 'rooms#show', @response.body + + get '/12345/rooms/1/edit' + assert_equal 'rooms#edit', @response.body + end +end -- cgit v1.2.3 From e296ea056e87027933c7d37e1e8c1f6ef73bc447 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 27 Oct 2009 19:32:31 -0500 Subject: Enable named route tests --- actionpack/test/dispatch/routing_test.rb | 280 ++++++++++++++++++------------- 1 file changed, 166 insertions(+), 114 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index ea7113a602..972bf73602 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -21,12 +21,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest } end end + old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, Dispatcher } - Routes = ActionDispatch::Routing::RouteSet.new - Routes.draw do + Routes.draw do |map| controller :sessions do get 'login', :to => :new, :as => :login post 'login', :to => :create @@ -123,214 +123,266 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest Routes end - def setup - Routes.install_helpers(metaclass) - end - def test_logout - delete '/logout' - assert_equal 'sessions#destroy', @response.body + with_test_routes do + delete '/logout' + assert_equal 'sessions#destroy', @response.body - # assert_equal '/logout', logout_path + assert_equal '/logout', logout_path + end end def test_login - get '/login' - assert_equal 'sessions#new', @response.body + with_test_routes do + get '/login' + assert_equal 'sessions#new', @response.body - post '/login' - assert_equal 'sessions#create', @response.body + post '/login' + assert_equal 'sessions#create', @response.body - # assert_equal '/login', app.login_path + assert_equal '/login', login_path + end end def test_login_redirect - get '/account/login' - assert_equal 301, @response.status - assert_equal 'http://www.example.com/login', @response.headers['Location'] - assert_equal 'Moved Permanently', @response.body + with_test_routes do + get '/account/login' + assert_equal 301, @response.status + assert_equal 'http://www.example.com/login', @response.headers['Location'] + assert_equal 'Moved Permanently', @response.body + end end def test_openid - get '/openid/login' - assert_equal 'openid#login', @response.body + with_test_routes do + get '/openid/login' + assert_equal 'openid#login', @response.body - post '/openid/login' - assert_equal 'openid#login', @response.body + post '/openid/login' + assert_equal 'openid#login', @response.body + end end + # TODO: rackmount is broken # def test_admin - # get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} - # assert_equal 'queenbee#index', @response.body + # with_test_routes do + # get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#index', @response.body # - # assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } # - # get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} - # assert_equal 'queenbee#accounts', @response.body + # get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + # assert_equal 'queenbee#accounts', @response.body # - # assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + # end # end def test_global - get '/global/dashboard' - assert_equal 'global#dashboard', @response.body + with_test_routes do + get '/global/dashboard' + assert_equal 'global#dashboard', @response.body - get '/global/export' - assert_equal 'global#export', @response.body + get '/global/export' + assert_equal 'global#export', @response.body - get '/global/hide_notice' - assert_equal 'global#hide_notice', @response.body + get '/global/hide_notice' + assert_equal 'global#hide_notice', @response.body - get '/export/123/foo.txt' - assert_equal 'global#export', @response.body + get '/export/123/foo.txt' + assert_equal 'global#export', @response.body - # assert_equal '/global/export', app.export_request_path - # assert_equal '/global/hide_notice', app.hide_notice_path - # assert_equal '/export/123/foo.txt', app.export_download_path(:id => 123, :file => 'foo.txt') + assert_equal '/global/export', export_request_path + assert_equal '/global/hide_notice', hide_notice_path + assert_equal '/export/123/foo.txt', export_download_path(:id => 123, :file => 'foo.txt') + end end def test_projects - get '/projects/1' - assert_equal 'projects#show', @response.body + with_test_routes do + get '/projects/1' + assert_equal 'projects#show', @response.body + end end def test_projects_involvements - get '/projects/1/involvements' - assert_equal 'involvements#index', @response.body + with_test_routes do + get '/projects/1/involvements' + assert_equal 'involvements#index', @response.body - get '/projects/1/involvements/1' - assert_equal 'involvements#show', @response.body + get '/projects/1/involvements/1' + assert_equal 'involvements#show', @response.body + end end def test_projects_attachments - get '/projects/1/attachments' - assert_equal 'attachments#index', @response.body + with_test_routes do + get '/projects/1/attachments' + assert_equal 'attachments#index', @response.body + end end def test_projects_participants - get '/projects/1/participants' - assert_equal 'participants#index', @response.body + with_test_routes do + get '/projects/1/participants' + assert_equal 'participants#index', @response.body - put '/projects/1/participants/update_all' - assert_equal 'participants#update_all', @response.body + put '/projects/1/participants/update_all' + assert_equal 'participants#update_all', @response.body + end end def test_projects_companies - get '/projects/1/companies' - assert_equal 'companies#index', @response.body + with_test_routes do + get '/projects/1/companies' + assert_equal 'companies#index', @response.body - get '/projects/1/companies/1/people' - assert_equal 'people#index', @response.body + get '/projects/1/companies/1/people' + assert_equal 'people#index', @response.body - get '/projects/1/companies/1/avatar' - assert_equal 'avatar#show', @response.body + get '/projects/1/companies/1/avatar' + assert_equal 'avatar#show', @response.body + end end def test_project_images - get '/projects/1/images' - assert_equal 'images#index', @response.body + with_test_routes do + get '/projects/1/images' + assert_equal 'images#index', @response.body - post '/projects/1/images/1/revise' - assert_equal 'images#revise', @response.body + post '/projects/1/images/1/revise' + assert_equal 'images#revise', @response.body + end end def test_projects_people - get '/projects/1/people' - assert_equal 'people#index', @response.body + with_test_routes do + get '/projects/1/people' + assert_equal 'people#index', @response.body - get '/projects/1/people/1' - assert_equal 'people#show', @response.body + get '/projects/1/people/1' + assert_equal 'people#show', @response.body - get '/projects/1/people/1/7a2dec8/avatar' - assert_equal 'avatar#show', @response.body + get '/projects/1/people/1/7a2dec8/avatar' + assert_equal 'avatar#show', @response.body - put '/projects/1/people/1/accessible_projects' - assert_equal 'people#accessible_projects', @response.body + put '/projects/1/people/1/accessible_projects' + assert_equal 'people#accessible_projects', @response.body - post '/projects/1/people/1/resend' - assert_equal 'people#resend', @response.body + post '/projects/1/people/1/resend' + assert_equal 'people#resend', @response.body - post '/projects/1/people/1/generate_new_password' - assert_equal 'people#generate_new_password', @response.body + post '/projects/1/people/1/generate_new_password' + assert_equal 'people#generate_new_password', @response.body + end end def test_projects_posts - get '/projects/1/posts' - assert_equal 'posts#index', @response.body + with_test_routes do + get '/projects/1/posts' + assert_equal 'posts#index', @response.body - get '/projects/1/posts/archive' - assert_equal 'posts#archive', @response.body + get '/projects/1/posts/archive' + assert_equal 'posts#archive', @response.body - get '/projects/1/posts/toggle_view' - assert_equal 'posts#toggle_view', @response.body + get '/projects/1/posts/toggle_view' + assert_equal 'posts#toggle_view', @response.body - post '/projects/1/posts/1/preview' - assert_equal 'posts#preview', @response.body + post '/projects/1/posts/1/preview' + assert_equal 'posts#preview', @response.body - get '/projects/1/posts/1/subscription' - assert_equal 'subscription#show', @response.body + get '/projects/1/posts/1/subscription' + assert_equal 'subscription#show', @response.body - get '/projects/1/posts/1/comments' - assert_equal 'comments#index', @response.body + get '/projects/1/posts/1/comments' + assert_equal 'comments#index', @response.body - post '/projects/1/posts/1/comments/preview' - assert_equal 'comments#preview', @response.body + post '/projects/1/posts/1/comments/preview' + assert_equal 'comments#preview', @response.body + end end def test_sprockets - get '/sprockets.js' - assert_equal 'javascripts', @response.body + with_test_routes do + get '/sprockets.js' + assert_equal 'javascripts', @response.body + end end def test_update_person_route - get '/people/1/update' - assert_equal 'people#update', @response.body + with_test_routes do + get '/people/1/update' + assert_equal 'people#update', @response.body - # assert_equal '/people/1/update', app.update_person_path(:id => 1) + assert_equal '/people/1/update', update_person_path(:id => 1) + end end def test_update_project_person - get '/projects/1/people/2/update' - assert_equal 'people#update', @response.body + with_test_routes do + get '/projects/1/people/2/update' + assert_equal 'people#update', @response.body - # assert_equal '/projects/1/people/2/update', app.update_project_person_path(:project_id => 1, :id => 2) + assert_equal '/projects/1/people/2/update', update_project_person_path(:project_id => 1, :id => 2) + end end def test_articles_perma - get '/articles/2009/08/18/rails-3' - assert_equal 'articles#show', @response.body + with_test_routes do + get '/articles/2009/08/18/rails-3' + assert_equal 'articles#show', @response.body - # assert_equal '/articles/2009/8/18/rails-3', app.article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') + assert_equal '/articles/2009/8/18/rails-3', article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') + end end def test_account_namespace - get '/account/subscription' - assert_equal 'subscription#show', @response.body + with_test_routes do + get '/account/subscription' + assert_equal 'subscription#show', @response.body - get '/account/credit' - assert_equal 'credit#show', @response.body + get '/account/credit' + assert_equal 'credit#show', @response.body - get '/account/credit_card' - assert_equal 'credit_card#show', @response.body + get '/account/credit_card' + assert_equal 'credit_card#show', @response.body + end end def test_articles_with_id - get '/articles/rails/1' - assert_equal 'articles#with_id', @response.body + with_test_routes do + get '/articles/rails/1' + assert_equal 'articles#with_id', @response.body - assert_raise(ActionController::RoutingError) { get '/articles/123/1' } + assert_raise(ActionController::RoutingError) { get '/articles/123/1' } - # assert_equal '/articles/rails/1', app.with_title_path(:title => 'rails', :id => 1) + assert_equal '/articles/rails/1', with_title_path(:title => 'rails', :id => 1) + end end def test_access_token_rooms - get '/12345/rooms' - assert_equal 'rooms#index', @response.body + with_test_routes do + get '/12345/rooms' + assert_equal 'rooms#index', @response.body - get '/12345/rooms/1' - assert_equal 'rooms#show', @response.body + get '/12345/rooms/1' + assert_equal 'rooms#show', @response.body - get '/12345/rooms/1/edit' - assert_equal 'rooms#edit', @response.body + get '/12345/rooms/1/edit' + assert_equal 'rooms#edit', @response.body + end end + + private + def with_test_routes + real_routes, temp_routes = ActionController::Routing::Routes, Routes + + ActionController::Routing.module_eval { remove_const :Routes } + ActionController::Routing.module_eval { const_set :Routes, temp_routes } + + yield + ensure + ActionController::Routing.module_eval { remove_const :Routes } + ActionController::Routing.const_set(:Routes, real_routes) + end end -- cgit v1.2.3 From 43e0bc1c9f39ebf5b253b498b38101a4aba04a45 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 27 Oct 2009 19:48:35 -0500 Subject: Extract routing controller stub helper into abstract unit --- actionpack/test/abstract_unit.rb | 20 +++++ actionpack/test/dispatch/routing_test.rb | 145 ++++++++++++++----------------- 2 files changed, 86 insertions(+), 79 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 86c8a95a43..214d79cd87 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -105,6 +105,26 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase self.app = build_app + class StubDispatcher + def self.new(*args) + lambda { |env| + params = env['action_dispatch.request.path_parameters'] + controller, action = params[:controller], params[:action] + [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] + } + end + end + + def self.stub_controllers + old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } + yield ActionDispatch::Routing::RouteSet.new + ensure + ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } + ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + end + def with_routing(&block) real_routes = ActionController::Routing::Routes ActionController::Routing.module_eval { remove_const :Routes } diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 972bf73602..5558cf0154 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -12,112 +12,99 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - class Dispatcher - def self.new(*args) - lambda { |env| - params = env['action_dispatch.request.path_parameters'] - controller, action = params[:controller], params[:action] - [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] - } - end - end - - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, Dispatcher } - Routes = ActionDispatch::Routing::RouteSet.new - Routes.draw do |map| - controller :sessions do - get 'login', :to => :new, :as => :login - post 'login', :to => :create - - delete 'logout', :to => :destroy, :as => :logout - end - - match 'account/login', :to => redirect("/login") - - match 'openid/login', :via => [:get, :post], :to => "openid#login" - - controller(:global) do - match 'global/:action' - match 'global/export', :to => :export, :as => :export_request - match 'global/hide_notice', :to => :hide_notice, :as => :hide_notice - match '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } - end - - constraints(:ip => /192\.168\.1\.\d\d\d/) do - get 'admin', :to => "queenbee#index" - end + stub_controllers do |routes| + Routes = routes + Routes.draw do |map| + controller :sessions do + get 'login', :to => :new, :as => :login + post 'login', :to => :create + + delete 'logout', :to => :destroy, :as => :logout + end - constraints IpRestrictor do - get 'admin/accounts', :to => "queenbee#accounts" - end + match 'account/login', :to => redirect("/login") - resources :projects, :controller => :project do - resources :involvements, :attachments + match 'openid/login', :via => [:get, :post], :to => "openid#login" - resources :participants do - put :update_all, :on => :collection + controller(:global) do + match 'global/:action' + match 'global/export', :to => :export, :as => :export_request + match 'global/hide_notice', :to => :hide_notice, :as => :hide_notice + match '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } end - resources :companies do - resources :people - resource :avatar + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin', :to => "queenbee#index" end - resources :images do - post :revise, :on => :member + constraints IpRestrictor do + get 'admin/accounts', :to => "queenbee#accounts" end - resources :people do - namespace ":access_token" do - resource :avatar + resources :projects, :controller => :project do + resources :involvements, :attachments + + resources :participants do + put :update_all, :on => :collection end - member do - put :accessible_projects - post :resend, :generate_new_password + resources :companies do + resources :people + resource :avatar + end + + resources :images do + post :revise, :on => :member + end + + resources :people do + namespace ":access_token" do + resource :avatar + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end end - end - resources :posts do - get :archive, :toggle_view, :on => :collection - post :preview, :on => :member + resources :posts do + get :archive, :toggle_view, :on => :collection + post :preview, :on => :member - resource :subscription + resource :subscription - resources :comments do - post :preview, :on => :collection + resources :comments do + post :preview, :on => :collection + end end end - end - match 'sprockets.js', :to => SprocketsApp + match 'sprockets.js', :to => SprocketsApp - match 'people/:id/update', :to => 'people#update', :as => :update_person - match '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person + match 'people/:id/update', :to => 'people#update', :as => :update_person + match '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person - # misc - match 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article + # misc + match 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article - namespace :account do - resource :subscription, :credit, :credit_card - end + namespace :account do + resource :subscription, :credit, :credit_card + end - controller :articles do - scope 'articles' do - scope ':title', :title => /[a-z]+/, :as => :with_title do - match ':id', :to => :with_id + controller :articles do + scope 'articles' do + scope ':title', :title => /[a-z]+/, :as => :with_title do + match ':id', :to => :with_id + end end end - end - scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do - resources :rooms + scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do + resources :rooms + end end end - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } def app Routes -- cgit v1.2.3 From 07da304ff84dca9a286005630cd3340f51384465 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 27 Oct 2009 19:57:57 -0500 Subject: Some more generation tests --- actionpack/test/dispatch/routing_test.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5558cf0154..74e6c8e72d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -116,6 +116,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'sessions#destroy', @response.body assert_equal '/logout', logout_path + assert_equal '/logout', url_for(:controller => 'sessions', :action => 'destroy', :only_path => true) end end @@ -123,11 +124,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest with_test_routes do get '/login' assert_equal 'sessions#new', @response.body + assert_equal '/login', login_path post '/login' assert_equal 'sessions#create', @response.body - assert_equal '/login', login_path + assert_equal '/login', url_for(:controller => 'sessions', :action => 'create', :only_path => true) + assert_equal '/login', url_for(:controller => 'sessions', :action => 'new', :only_path => true) end end -- cgit v1.2.3 From cbcb947b00a7c6992cfe42c6b369e87b4fa4ee23 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Tue, 27 Oct 2009 21:01:31 -0700 Subject: AS::Notifications.subscribe blocks are now yielded the arguments to pass to AS::Notifications::Event.new --- actionpack/lib/action_controller/notifications.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/notifications.rb b/actionpack/lib/action_controller/notifications.rb index 4ec88193d5..1a4f29e0e2 100644 --- a/actionpack/lib/action_controller/notifications.rb +++ b/actionpack/lib/action_controller/notifications.rb @@ -1,6 +1,8 @@ require 'active_support/notifications' -ActiveSupport::Notifications.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |event| +ActiveSupport::Notifications.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + if logger = ActionController::Base.logger human_name = event.name.to_s.humanize logger.info("#{human_name} (%.1fms)" % event.duration) -- cgit v1.2.3 From 0b2dd7afd9ee1dfe46506f9d745afb0a23e496ba Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 28 Oct 2009 00:12:35 -0700 Subject: Reorganize CSRF a bit --- .../metal/request_forgery_protection.rb | 56 +++++++++------------- actionpack/lib/action_dispatch/http/request.rb | 4 ++ 2 files changed, 27 insertions(+), 33 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index ad06657f86..113c20a758 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,7 +5,6 @@ module ActionController #:nodoc: module RequestForgeryProtection extend ActiveSupport::Concern - # TODO : Remove the defined? check when new base is the main base include AbstractController::Helpers, Session included do @@ -21,26 +20,26 @@ module ActionController #:nodoc: helper_method :protect_against_forgery? end - # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a - # forged link from another site, is done by embedding a token based on a random string stored in the session (which an attacker wouldn't know) in all - # forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only - # HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication - # scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway. + # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current + # web application, not a forged link from another site, is done by embedding a token based on a random + # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated + # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript + # requests are checked, so this will not protect your XML API (presumably you'll have a different + # authentication scheme there anyway). Also, GET requests are not protected as these should be + # idempotent anyway. # # This is turned on with the protect_from_forgery method, which will check the token and raise an - # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the error message in - # production by editing public/422.html. A call to this method in ApplicationController is generated by default in post-Rails 2.0 - # applications. + # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the + # error message in production by editing public/422.html. A call to this method in ApplicationController is + # generated by default in post-Rails 2.0 applications. # - # The token parameter is named authenticity_token by default. If you are generating an HTML form manually (without the - # use of Rails' form_for, form_tag or other helpers), you have to include a hidden field named like that and - # set its value to what is returned by form_authenticity_token. Same applies to manually constructed Ajax requests. To - # make the token available through a global variable to scripts on a certain page, you could add something like this to a view: + # The token parameter is named authenticity_token by default. If you are generating an HTML form + # manually (without the use of Rails' form_for, form_tag or other helpers), you have to + # include a hidden field named like that and set its value to what is returned by + # form_authenticity_token. # - # <%= javascript_tag "window._token = '#{form_authenticity_token}'" %> - # - # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails 1.x, add this to - # config/environments/test.rb: + # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails + # 1.x, add this to config/environments/test.rb: # # # Disable request forgery protection in test environment # config.action_controller.allow_forgery_protection = false @@ -57,7 +56,8 @@ module ActionController #:nodoc: # * Keep your GET requests safe and idempotent. More reading material: # * http://www.xml.com/pub/a/2002/04/24/deviant.html # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 - # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look for "Expires: at end of session" + # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look + # for "Expires: at end of session" # module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. @@ -76,10 +76,7 @@ module ActionController #:nodoc: # * :only/:except - Passed to the before_filter call. Set which actions are verified. def protect_from_forgery(options = {}) self.request_forgery_protection_token ||= :authenticity_token - before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except) - if options[:secret] || options[:digest] - ActiveSupport::Deprecation.warn("protect_from_forgery only takes :only and :except options now. :digest and :secret have no effect", caller) - end + before_filter :verify_authenticity_token, options end end @@ -88,31 +85,24 @@ module ActionController #:nodoc: def verify_authenticity_token verified_request? || raise(ActionController::InvalidAuthenticityToken) end - + # Returns true or false if a request is verified. Checks: # # * is the format restricted? By default, only HTML requests are checked. # * is it a GET request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? def verified_request? - !protect_against_forgery? || - request.method == :get || - request.xhr? || - !verifiable_request_format? || + !protect_against_forgery? || request.forgery_whitelisted? || form_authenticity_token == params[request_forgery_protection_token] end - - def verifiable_request_format? - !request.content_type.nil? && request.content_type.verify_request? - end - + # Sets the token value for the current session. def form_authenticity_token session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32) end def protect_against_forgery? - allow_forgery_protection && request_forgery_protection_token + allow_forgery_protection end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 1e366520c9..bb99fac5e0 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -97,6 +97,10 @@ module ActionDispatch end end + def forgery_whitelisted? + method == :get || xhr? || !(!content_type.nil? && content_type.verify_request?) + end + def media_type content_type.to_s end -- cgit v1.2.3 From c5e73b897616049c613b0331dd53e88dbc9c1532 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 28 Oct 2009 00:13:08 -0700 Subject: Reduce TextTemplate cost for simple cases --- actionpack/lib/action_dispatch/http/mime_type.rb | 1 + actionpack/lib/action_view/template/text.rb | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index e85823d8db..c30897b32a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -24,6 +24,7 @@ module Mime LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } def self.[](type) + return type if type.is_a?(Type) Type.lookup_by_extension(type.to_s) end diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index f7d0df5ba0..fa8cfe506b 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -1,6 +1,8 @@ module ActionView #:nodoc: class TextTemplate < String #:nodoc: - def initialize(string, content_type = Mime[:html]) + HTML = Mime[:html] + + def initialize(string, content_type = HTML) super(string.to_s) @content_type = Mime[content_type] || content_type end -- cgit v1.2.3 From 654b33afc5a789aea90199bbb108093a80fe8020 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 28 Oct 2009 01:43:46 -0700 Subject: New semantics eliminate the need for __send__ --- actionpack/lib/action_controller/metal/verification.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/metal/verification.rb b/actionpack/lib/action_controller/metal/verification.rb index d3d78e3749..500cced539 100644 --- a/actionpack/lib/action_controller/metal/verification.rb +++ b/actionpack/lib/action_controller/metal/verification.rb @@ -79,8 +79,8 @@ module ActionController #:nodoc: # do not apply this verification to the actions specified in the associated # array (may also be a single value). def verify(options={}) - before_filter :only => options[:only], :except => options[:except] do |c| - c.__send__ :verify_action, options + before_filter :only => options[:only], :except => options[:except] do + verify_action options end end end -- cgit v1.2.3 From 427a7385eb9b784ad4372bf607217b0b7b2ca543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Oct 2009 14:13:48 -0500 Subject: Make polymorphic_url work with symbols again and refactor it [#1384 status:resolved] Signed-off-by: Joshua Peek --- .../lib/action_controller/polymorphic_routes.rb | 41 ++++------------------ .../test/activerecord/polymorphic_routes_test.rb | 14 ++++---- 2 files changed, 13 insertions(+), 42 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb index 2adf3575a7..eaed00cfb7 100644 --- a/actionpack/lib/action_controller/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -80,9 +80,8 @@ module ActionController record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 end - record = extract_record(record_or_hash_or_array) - record = record.to_model if record.respond_to?(:to_model) - namespace = extract_namespace(record_or_hash_or_array) + record = extract_record(record_or_hash_or_array) + record = record.to_model if record.respond_to?(:to_model) args = case record_or_hash_or_array when Hash; [ record_or_hash_or_array ] @@ -105,8 +104,7 @@ module ActionController end args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - - named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) + named_route = build_named_route_call(record_or_hash_or_array, inflection, options) url_options = options.except(:action, :routing_type) unless url_options.empty? @@ -138,18 +136,6 @@ module ActionController EOT end - def formatted_polymorphic_url(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) - options[:format] = record_or_hash.pop if Array === record_or_hash - polymorphic_url(record_or_hash, options) - end - - def formatted_polymorphic_path(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) - options[:format] = record_or_hash.pop if record_or_hash === Array - polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) - end - private def action_prefix(options) options[:action] ? "#{options[:action]}_" : '' @@ -159,7 +145,7 @@ module ActionController options[:routing_type] || :url end - def build_named_route_call(records, namespace, inflection, options = {}) + def build_named_route_call(records, inflection, options = {}) unless records.is_a?(Array) record = extract_record(records) route = '' @@ -169,7 +155,7 @@ module ActionController if parent.is_a?(Symbol) || parent.is_a?(String) string << "#{parent}_" else - string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize + string << RecordIdentifier.__send__("plural_class_name", parent).singularize string << "_" end end @@ -178,12 +164,12 @@ module ActionController if record.is_a?(Symbol) || record.is_a?(String) route << "#{record}_" else - route << "#{RecordIdentifier.__send__("plural_class_name", record)}" + route << RecordIdentifier.__send__("plural_class_name", record) route = route.singularize if inflection == :singular route << "_" end - action_prefix(options) + namespace + route + routing_type(options).to_s + action_prefix(options) + route + routing_type(options).to_s end def extract_record(record_or_hash_or_array) @@ -193,18 +179,5 @@ module ActionController else record_or_hash_or_array end end - - # Remove the first symbols from the array and return the url prefix - # implied by those symbols. - def extract_namespace(record_or_hash_or_array) - return "" unless record_or_hash_or_array.is_a?(Array) - - namespace_keys = [] - while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) - namespace_keys << record_or_hash_or_array.shift - end - - namespace_keys.map {|k| "#{k}_"}.join - end end end diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb index 37f1f6dff8..ad744421db 100644 --- a/actionpack/test/activerecord/polymorphic_routes_test.rb +++ b/actionpack/test/activerecord/polymorphic_routes_test.rb @@ -98,14 +98,6 @@ class PolymorphicRoutesTest < ActionController::TestCase end end - def test_formatted_url_helper_is_deprecated - with_test_routes do - assert_deprecated do - formatted_polymorphic_url([@project, :pdf]) - end - end - end - def test_format_option with_test_routes do @project.save @@ -251,6 +243,12 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def test_with_array_containing_symbols + with_test_routes do + assert_equal "http://example.com/series/new", polymorphic_url([:new, :series]) + end + end + def test_with_hash with_test_routes do @project.save -- cgit v1.2.3 From 8dcf91ca113579646e95b0fd7a864dfb6512a53b Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 28 Oct 2009 16:53:34 -0400 Subject: First pass at cleaning up action caching --- actionpack/lib/action_controller/caching.rb | 28 ++-- .../lib/action_controller/caching/actions.rb | 172 ++++++++++----------- actionpack/lib/action_view/render/rendering.rb | 1 - 3 files changed, 96 insertions(+), 105 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 38cf1da6a8..63429e1cbe 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -52,19 +52,23 @@ module ActionController #:nodoc: end end - protected - # Convenience accessor - def cache(key, options = {}, &block) - if cache_configured? - cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) - else - yield - end - end + def caching_allowed? + request.get? && response.status == 200 + end - private - def cache_configured? - self.class.cache_configured? + protected + # Convenience accessor + def cache(key, options = {}, &block) + if cache_configured? + cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) + else + yield end + end + + private + def cache_configured? + self.class.cache_configured? + end end end diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index cb0c3a1384..05305d9a3f 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -2,9 +2,10 @@ require 'set' module ActionController #:nodoc: module Caching - # Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching, - # every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which - # allows for authentication and other restrictions on whether someone is allowed to see the cache. Example: + # Action caching is similar to page caching by the fact that the entire output of the response is + # cached, but unlike page caching, every request still goes through the Action Pack. The key benefit + # of this is that filters are run before the cache is served, which allows for authentication and other + # restrictions on whether someone is allowed to see the cache. Example: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public @@ -12,44 +13,53 @@ module ActionController #:nodoc: # caches_action :index, :show, :feed # end # - # In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the - # show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches. + # In this example, the public action doesn't require authentication, so it's possible to use the faster + # page caching method. But both the show and feed action are to be shielded behind the authenticate + # filter, so we need to implement those as action caches. # - # Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both - # the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named - # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and - # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern. + # Action caching internally uses the fragment caching and an around filter to do the job. The fragment + # cache is named according to both the current host and the path. So a page that is accessed at + # http://david.somewhere.com/lists/show/1 will result in a fragment named + # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between + # "david.somewhere.com/lists/" and + # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key + # pattern. # - # Different representations of the same resource, e.g. http://david.somewhere.com/lists and http://david.somewhere.com/lists.xml - # are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that :action => 'lists' is not the same - # as :action => 'list', :format => :xml. + # Different representations of the same resource, e.g. http://david.somewhere.com/lists and + # http://david.somewhere.com/lists.xml + # are treated like separate requests and so are cached separately. Keep in mind when expiring an + # action cache that :action => 'lists' is not the same as + # :action => 'list', :format => :xml. # - # You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy - # for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance. + # You can set modify the default action cache path by passing a :cache_path option. This will be + # passed directly to ActionCachePath.path_for. This is handy for actions with multiple possible + # routes that should be cached differently. If a block is given, it is called with the current + # controller instance. # - # And you can also use :if (or :unless) to pass a Proc that specifies when the action should be cached. + # And you can also use :if (or :unless) to pass a Proc that specifies when the action should + # be cached. # # Finally, if you are using memcached, you can also pass :expires_in. # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public # caches_page :public - # caches_action :index, :if => Proc.new { |c| !c.request.format.json? } # cache if is not a JSON request + # caches_action :index, :if => proc { |c| !c.request.format.json? } # cache if is not a JSON request # caches_action :show, :cache_path => { :project => 1 }, :expires_in => 1.hour - # caches_action :feed, :cache_path => Proc.new { |controller| + # caches_action :feed, :cache_path => proc { |controller| # controller.params[:user_id] ? # controller.send(:user_list_url, controller.params[:user_id], controller.params[:id]) : # controller.send(:list_url, controller.params[:id]) } # end # - # If you pass :layout => false, it will only cache your action content. It is useful when your layout has dynamic information. + # If you pass :layout => false, it will only cache your action content. It is useful when your + # layout has dynamic information. # module Actions - def self.included(base) #:nodoc: - base.extend(ClassMethods) - base.class_eval do - attr_accessor :rendered_action_cache, :action_cache_path - end + extend ActiveSupport::Concern + + included do + attr_accessor :rendered_action_cache, :action_cache_path end module ClassMethods @@ -58,22 +68,35 @@ module ActionController #:nodoc: def caches_action(*actions) return unless cache_configured? options = actions.extract_options! - filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) } + filter_options = options.extract!(:if, :unless).merge(:only => actions) + cache_options = options.extract!(:layout, :cache_path).merge(:store_options => options) - cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options) - - around_filter cache_filter, filter_options + around_filter ActionCacheFilter.new(cache_options), filter_options end end + def _render_cache_fragment(cache, extension, layout) + self.rendered_action_cache = true + response.content_type = Mime[extension].to_s if extension + options = { :text => cache } + options.merge!(:layout => true) if layout + render options + end + + def _save_fragment(name, layout, options) + return unless caching_allowed? + + content = layout ? view_context.content_for(:layout) : response_body + write_fragment(name, content, options) + end + protected def expire_action(options = {}) return unless cache_configured? - if options[:action].is_a?(Array) - options[:action].dup.each do |action| - expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }), false)) - end + actions = options[:action] + if actions.is_a?(Array) + actions.each {|action| expire_action(options.merge(:action => action)) } else expire_fragment(ActionCachePath.path_for(self, options, false)) end @@ -81,57 +104,21 @@ module ActionController #:nodoc: class ActionCacheFilter #:nodoc: def initialize(options, &block) - @options = options + @cache_path, @store_options, @layout = + options.values_at(:cache_path, :store_options, :layout) end def filter(controller) - should_continue = before(controller) - yield if should_continue - after(controller) - end - - def before(controller) - cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path))) + path_options = @cache_path.respond_to?(:call) ? @cache_path.call(controller) : @cache_path + cache_path = ActionCachePath.new(controller, path_options || {}) - if cache = controller.read_fragment(cache_path.path, @options[:store_options]) - controller.rendered_action_cache = true - set_content_type!(controller, cache_path.extension) - options = { :text => cache } - options.merge!(:layout => true) if cache_layout? - controller.__send__(:render, options) - false + if cache = controller.read_fragment(cache_path.path, @store_options) + controller._render_cache_fragment(cache, cache_path.extension, @layout == false) else - controller.action_cache_path = cache_path + yield + controller._save_fragment(cache_path.path, @layout == false, @store_options) end end - - def after(controller) - return if controller.rendered_action_cache || !caching_allowed(controller) - action_content = cache_layout? ? content_for_layout(controller) : controller.response.body - controller.write_fragment(controller.action_cache_path.path, action_content, @options[:store_options]) - end - - private - def set_content_type!(controller, extension) - controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension - end - - def path_options_for(controller, options) - ((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {} - end - - def caching_allowed(controller) - controller.request.get? && controller.response.status.to_i == 200 - end - - def cache_layout? - @options[:layout] == false - end - - def content_for_layout(controller) - template = controller.view_context - template.layout && template.instance_variable_get('@cached_content_for_layout') - end end class ActionCachePath @@ -142,10 +129,11 @@ module ActionController #:nodoc: new(controller, options, infer_extension).path end end - - # If +infer_extension+ is true, the cache path extension is looked up from the request's path & format. - # This is desirable when reading and writing the cache, but not when expiring the cache - - # expire_action should expire the same files regardless of the request format. + + # If +infer_extension+ is true, the cache path extension is looked up from the request's + # path & format. This is desirable when reading and writing the cache, but not when + # expiring the cache - expire_action should expire the same files regardless of the + # request format. def initialize(controller, options = {}, infer_extension = true) if infer_extension extract_extension(controller.request) @@ -158,20 +146,20 @@ module ActionController #:nodoc: @path = URI.unescape(path) end - private - def normalize!(path) - path << 'index' if path[-1] == ?/ - end + private + def normalize!(path) + path << 'index' if path[-1] == ?/ + end - def add_extension!(path, extension) - path << ".#{extension}" if extension and !path.ends_with?(extension) - end - - def extract_extension(request) - # Don't want just what comes after the last '.' to accommodate multi part extensions - # such as tar.gz. - @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format - end + def add_extension!(path, extension) + path << ".#{extension}" if extension and !path.ends_with?(extension) + end + + def extract_extension(request) + # Don't want just what comes after the last '.' to accommodate multi part extensions + # such as tar.gz. + @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format + end end end end diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index b6f5b9b6d1..7dcd9d38d9 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -123,7 +123,6 @@ module ActionView template.render(self, locals) end - @cached_content_for_layout = content @_content_for[:layout] = content if layout -- cgit v1.2.3 From 51c24ae3e3c1a260a304042aff5e1a7c56faabfe Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 29 Oct 2009 00:37:29 -0400 Subject: Caching refactoring --- actionpack/lib/action_controller/caching.rb | 32 +++-- .../lib/action_controller/caching/actions.rb | 135 ++++++++++----------- .../lib/action_controller/caching/sweeping.rb | 2 +- .../lib/action_controller/metal/compatibility.rb | 8 +- actionpack/lib/action_dispatch/http/request.rb | 6 +- actionpack/test/controller/caching_test.rb | 8 +- 6 files changed, 98 insertions(+), 93 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 63429e1cbe..083d6328af 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -3,26 +3,30 @@ require 'uri' require 'set' module ActionController #:nodoc: - # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls - # around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment. + # Caching is a cheap way of speeding up slow applications by keeping the result of + # calculations, renderings, and database calls around for subsequent requests. + # Action Controller affords you three approaches in varying levels of granularity: + # Page, Action, Fragment. # - # You can read more about each approach and the sweeping assistance by clicking the modules below. - # - # Note: To turn off all caching and sweeping, set Base.perform_caching = false. + # You can read more about each approach and the sweeping assistance by clicking the + # modules below. # + # Note: To turn off all caching and sweeping, set + # config.action_controller.perform_caching = false. # # == Caching stores # - # All the caching stores from ActiveSupport::Cache are available to be used as backends for Action Controller caching. This setting only - # affects action and fragment caching as page caching is always written to disk. + # All the caching stores from ActiveSupport::Cache are available to be used as backends + # for Action Controller caching. This setting only affects action and fragment caching + # as page caching is always written to disk. # # Configuration examples (MemoryStore is the default): # - # ActionController::Base.cache_store = :memory_store - # ActionController::Base.cache_store = :file_store, "/path/to/cache/directory" - # ActionController::Base.cache_store = :drb_store, "druby://localhost:9192" - # ActionController::Base.cache_store = :mem_cache_store, "localhost" - # ActionController::Base.cache_store = MyOwnStore.new("parameter") + # config.action_controller.cache_store = :memory_store + # config.action_controller.cache_store = :file_store, "/path/to/cache/directory" + # config.action_controller.cache_store = :drb_store, "druby://localhost:9192" + # config.action_controller.cache_store = :mem_cache_store, "localhost" + # config.action_controller.cache_store = MyOwnStore.new("parameter") module Caching extend ActiveSupport::Concern @@ -46,8 +50,10 @@ module ActionController #:nodoc: @@perform_caching = true cattr_accessor :perform_caching + end - def self.cache_configured? + module ClassMethods + def cache_configured? perform_caching && cache_store end end diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 05305d9a3f..35111a4b92 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -2,10 +2,12 @@ require 'set' module ActionController #:nodoc: module Caching - # Action caching is similar to page caching by the fact that the entire output of the response is - # cached, but unlike page caching, every request still goes through the Action Pack. The key benefit - # of this is that filters are run before the cache is served, which allows for authentication and other - # restrictions on whether someone is allowed to see the cache. Example: + # Action caching is similar to page caching by the fact that the entire + # output of the response is cached, but unlike page caching, every + # request still goes through the Action Pack. The key benefit + # of this is that filters are run before the cache is served, which + # allows for authentication and other restrictions on whether someone + # is allowed to see the cache. Example: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public @@ -13,55 +15,65 @@ module ActionController #:nodoc: # caches_action :index, :show, :feed # end # - # In this example, the public action doesn't require authentication, so it's possible to use the faster - # page caching method. But both the show and feed action are to be shielded behind the authenticate + # In this example, the public action doesn't require authentication, + # so it's possible to use the faster page caching method. But both + # the show and feed action are to be shielded behind the authenticate # filter, so we need to implement those as action caches. # - # Action caching internally uses the fragment caching and an around filter to do the job. The fragment - # cache is named according to both the current host and the path. So a page that is accessed at + # Action caching internally uses the fragment caching and an around + # filter to do the job. The fragment cache is named according to both + # the current host and the path. So a page that is accessed at # http://david.somewhere.com/lists/show/1 will result in a fragment named - # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between - # "david.somewhere.com/lists/" and - # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key - # pattern. + # "david.somewhere.com/lists/show/1". This allows the cacher to + # differentiate between "david.somewhere.com/lists/" and + # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting + # the subdomain-as-account-key pattern. # - # Different representations of the same resource, e.g. http://david.somewhere.com/lists and + # Different representations of the same resource, e.g. + # http://david.somewhere.com/lists and # http://david.somewhere.com/lists.xml - # are treated like separate requests and so are cached separately. Keep in mind when expiring an - # action cache that :action => 'lists' is not the same as + # are treated like separate requests and so are cached separately. + # Keep in mind when expiring an action cache that + # :action => 'lists' is not the same as # :action => 'list', :format => :xml. # - # You can set modify the default action cache path by passing a :cache_path option. This will be - # passed directly to ActionCachePath.path_for. This is handy for actions with multiple possible - # routes that should be cached differently. If a block is given, it is called with the current - # controller instance. + # You can set modify the default action cache path by passing a + # :cache_path option. This will be passed directly to + # ActionCachePath.path_for. This is handy for actions with multiple + # possible routes that should be cached differently. If a block is + # given, it is called with the current controller instance. # - # And you can also use :if (or :unless) to pass a Proc that specifies when the action should - # be cached. + # And you can also use :if (or :unless) to pass a Proc that + # specifies when the action should be cached. # # Finally, if you are using memcached, you can also pass :expires_in. # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public # caches_page :public - # caches_action :index, :if => proc { |c| !c.request.format.json? } # cache if is not a JSON request - # caches_action :show, :cache_path => { :project => 1 }, :expires_in => 1.hour - # caches_action :feed, :cache_path => proc { |controller| - # controller.params[:user_id] ? - # controller.send(:user_list_url, controller.params[:user_id], controller.params[:id]) : - # controller.send(:list_url, controller.params[:id]) } + # caches_action :index, :if => proc do |c| + # !c.request.format.json? # cache if is not a JSON request + # end + # + # caches_action :show, :cache_path => { :project => 1 }, + # :expires_in => 1.hour + # + # caches_action :feed, :cache_path => proc do |controller| + # if controller.params[:user_id] + # controller.send(:user_list_url, + # controller.params[:user_id], controller.params[:id]) + # else + # controller.send(:list_url, controller.params[:id]) + # end + # end # end # - # If you pass :layout => false, it will only cache your action content. It is useful when your - # layout has dynamic information. + # If you pass :layout => false, it will only cache your action + # content. It is useful when your layout has dynamic information. # module Actions extend ActiveSupport::Concern - included do - attr_accessor :rendered_action_cache, :action_cache_path - end - module ClassMethods # Declares that +actions+ should be cached. # See ActionController::Caching::Actions for details. @@ -76,11 +88,7 @@ module ActionController #:nodoc: end def _render_cache_fragment(cache, extension, layout) - self.rendered_action_cache = true - response.content_type = Mime[extension].to_s if extension - options = { :text => cache } - options.merge!(:layout => true) if layout - render options + render :text => cache, :layout => layout, :content_type => Mime[extension || :html] end def _save_fragment(name, layout, options) @@ -90,17 +98,17 @@ module ActionController #:nodoc: write_fragment(name, content, options) end - protected - def expire_action(options = {}) - return unless cache_configured? + protected + def expire_action(options = {}) + return unless cache_configured? - actions = options[:action] - if actions.is_a?(Array) - actions.each {|action| expire_action(options.merge(:action => action)) } - else - expire_fragment(ActionCachePath.path_for(self, options, false)) - end + actions = options[:action] + if actions.is_a?(Array) + actions.each {|action| expire_action(options.merge(:action => action)) } + else + expire_fragment(ActionCachePath.new(self, options, false).path) end + end class ActionCacheFilter #:nodoc: def initialize(options, &block) @@ -109,7 +117,12 @@ module ActionController #:nodoc: end def filter(controller) - path_options = @cache_path.respond_to?(:call) ? @cache_path.call(controller) : @cache_path + path_options = if @cache_path.respond_to?(:call) + controller.instance_exec(controller, &@cache_path) + else + @cache_path + end + cache_path = ActionCachePath.new(controller, path_options || {}) if cache = controller.read_fragment(cache_path.path, @store_options) @@ -124,41 +137,25 @@ module ActionController #:nodoc: class ActionCachePath attr_reader :path, :extension - class << self - def path_for(controller, options, infer_extension = true) - new(controller, options, infer_extension).path - end - end - # If +infer_extension+ is true, the cache path extension is looked up from the request's # path & format. This is desirable when reading and writing the cache, but not when # expiring the cache - expire_action should expire the same files regardless of the # request format. def initialize(controller, options = {}, infer_extension = true) if infer_extension - extract_extension(controller.request) - options = options.reverse_merge(:format => @extension) if options.is_a?(Hash) + @extension = controller.params[:format] + options.reverse_merge!(:format => @extension) if options.is_a?(Hash) end - path = controller.url_for(options).split('://').last - normalize!(path) - add_extension!(path, @extension) - @path = URI.unescape(path) + path = controller.url_for(options).split(%r{://}).last + @path = normalize!(path) end private def normalize!(path) path << 'index' if path[-1] == ?/ - end - - def add_extension!(path, extension) path << ".#{extension}" if extension and !path.ends_with?(extension) - end - - def extract_extension(request) - # Don't want just what comes after the last '.' to accommodate multi part extensions - # such as tar.gz. - @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format + URI.unescape(path) end end end diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index c1be264ffb..871f41bfdd 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -70,7 +70,7 @@ module ActionController #:nodoc: protected # gets the action cache path for the given options. def action_path_for(options) - ActionController::Caching::Actions::ActionCachePath.path_for(controller, options) + Actions::ActionCachePath.new(controller, options).path end # Retrieve instance variables set in the controller. diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index 22f9ab219c..c251d79f4e 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -25,9 +25,11 @@ module ActionController # cattr_reader :protected_instance_variables cattr_accessor :protected_instance_variables - self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller - @action_name @before_filter_chain_aborted @action_cache_path @_headers @_params - @_flash @_response) + self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render + @variables_added @request_origin @url + @parent_controller @action_name + @before_filter_chain_aborted @_headers @_params + @_flash @_response) # Indicates whether or not optimise the generated named # route helper methods diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index bb99fac5e0..75be2cc260 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -98,7 +98,7 @@ module ActionDispatch end def forgery_whitelisted? - method == :get || xhr? || !(!content_type.nil? && content_type.verify_request?) + method == :get || xhr? || content_type.nil? || !content_type.verify_request? end def media_type @@ -205,10 +205,6 @@ module ActionDispatch end end - def cache_format - parameters[:format] - end - # Returns true if the request's "X-Requested-With" header contains # "XMLHttpRequest". (The Prototype Javascript library sends this header with # every Ajax request.) diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index df2dee8228..3ce90b6ccf 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -228,12 +228,16 @@ class ActionCachingMockController @mock_url_for end + def params + request.parameters + end + def request mocked_path = @mock_path Object.new.instance_eval(<<-EVAL) def path; '#{@mock_path}' end def format; 'all' end - def cache_format; nil end + def parameters; {:format => nil}; end self EVAL end @@ -466,7 +470,7 @@ class ActionCacheTest < ActionController::TestCase @mock_controller.mock_url_for = 'http://example.org/' @mock_controller.mock_path = '/' - assert_equal 'example.org/index', @path_class.path_for(@mock_controller, {}) + assert_equal 'example.org/index', @path_class.new(@mock_controller, {}).path end def test_file_extensions -- cgit v1.2.3 From a9751a7034c5a2a49fd90e9f79ad5fcae103487b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 17 Oct 2009 10:31:11 -0300 Subject: Refactor ActionMailer layout and remove legacy one. --- actionpack/lib/abstract_controller/layouts.rb | 20 ++ actionpack/lib/action_controller/legacy/layout.rb | 256 ---------------------- actionpack/lib/action_controller/metal/layouts.rb | 11 - 3 files changed, 20 insertions(+), 267 deletions(-) delete mode 100644 actionpack/lib/action_controller/legacy/layout.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 796ef40584..4723e18a01 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -171,6 +171,26 @@ module AbstractController name && _find_layout(name, details) end + # Determine the layout for a given name and details, taking into account + # the name type. + # + # ==== Parameters + # name:: The name of the template + # details Object}>:: A list of details to restrict + # the lookup to. By default, layout lookup is limited to the + # formats specified for the current request. + def _layout_for_option(name, details) + case name + when String then _layout_for_name(name, details) + when true then _default_layout(details, true) + when :default then _default_layout(details, false) + when false, nil then nil + else + raise ArgumentError, + "String, true, or false, expected for `layout'; you passed #{name.inspect}" + end + end + # Take in the name and details and find a Template. # # ==== Parameters diff --git a/actionpack/lib/action_controller/legacy/layout.rb b/actionpack/lib/action_controller/legacy/layout.rb deleted file mode 100644 index 4e3b67de20..0000000000 --- a/actionpack/lib/action_controller/legacy/layout.rb +++ /dev/null @@ -1,256 +0,0 @@ -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/class' -require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/class/inheritable_attributes' - -module ActionController #:nodoc: - # MegasuperultraHAX - # plz refactor ActionMailer - class Base - @@exempt_from_layout = [ActionView::TemplateHandlers::RJS] - cattr_accessor :exempt_from_layout - end - - module Layout #:nodoc: - def self.included(base) - base.extend(ClassMethods) - base.class_inheritable_accessor :layout_name, :layout_conditions - end - - # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in - # repeated setups. The inclusion pattern has pages that look like this: - # - # <%= render "shared/header" %> - # Hello World - # <%= render "shared/footer" %> - # - # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose - # and if you ever want to change the structure of these two includes, you'll have to change all the templates. - # - # With layouts, you can flip it around and have the common structure know where to insert changing content. This means - # that the header and footer are only mentioned in one place, like this: - # - # // The header part of this layout - # <%= yield %> - # // The footer part of this layout - # - # And then you have content pages that look like this: - # - # hello world - # - # At rendering time, the content page is computed and then inserted in the layout, like this: - # - # // The header part of this layout - # hello world - # // The footer part of this layout - # - # == Accessing shared variables - # - # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with - # references that won't materialize before rendering time: - # - #

<%= @page_title %>

- # <%= yield %> - # - # ...and content pages that fulfill these references _at_ rendering time: - # - # <% @page_title = "Welcome" %> - # Off-world colonies offers you a chance to start a new life - # - # The result after rendering is: - # - #

Welcome

- # Off-world colonies offers you a chance to start a new life - # - # == Automatic layout assignment - # - # If there is a template in app/views/layouts/ with the same name as the current controller then it will be automatically - # set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named - # app/views/layouts/weblog.erb or app/views/layouts/weblog.builder exists then it will be automatically set as - # the layout for your WeblogController. You can create a layout with the name application.erb or application.builder - # 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 - # no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout. - # assignment. So an Admin::WeblogController will look for a template named app/views/layouts/admin/weblog.erb. - # Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set. - # Explicitly setting the layout in a parent class, though, will not override the child class's layout assignment if the child - # class has a layout with the same name. - # - # == Inheritance for layouts - # - # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples: - # - # class BankController < ActionController::Base - # layout "bank_standard" - # - # class InformationController < BankController - # - # class VaultController < BankController - # layout :access_level_layout - # - # class EmployeeController < BankController - # layout nil - # - # The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites - # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all. - # - # == Types of layouts - # - # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes - # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can - # be done either by specifying a method reference as a symbol or using an inline method (as a proc). - # - # The method reference is the preferred approach to variable layouts and is used like this: - # - # class WeblogController < ActionController::Base - # layout :writers_and_readers - # - # def index - # # fetching posts - # end - # - # private - # def writers_and_readers - # logged_in? ? "writer_layout" : "reader_layout" - # end - # - # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing - # is logged in or not. - # - # If you want to use an inline method, such as a proc, do something like this: - # - # class WeblogController < ActionController::Base - # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } - # - # Of course, the most common way of specifying a layout is still just as a plain template name: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard" - # - # If no directory is specified for the template name, the template will by default be looked for in app/views/layouts/. - # Otherwise, it will be looked up relative to the template root. - # - # == Conditional layouts - # - # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering - # 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 - # :only and :except options can be passed to the layout call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard", :except => :rss - # - # # ... - # - # end - # - # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout - # around the rendered view. - # - # Both the :only and :except condition can accept an arbitrary number of method references, so - # #:except => [ :rss, :text_only ] is valid, as is :except => :rss. - # - # == Using a different layout in the action render call - # - # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above. - # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller. - # You can do this by passing a :layout option to the render call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard" - # - # def help - # render :action => "help", :layout => "help" - # end - # end - # - # This will render the help action with the "help" layout instead of the controller-wide "weblog_standard" layout. - module ClassMethods - extend ActiveSupport::Memoizable - - # If a layout is specified, all rendered actions will have their result rendered - # when the layout yields. This layout can itself depend on instance variables assigned during action - # performance and have access to them as any normal template would. - def layout(template_name, conditions = {}, auto = false) - add_layout_conditions(conditions) - self.layout_name = template_name - end - - def memoized_default_layout(formats) #:nodoc: - self.layout_name || begin - layout = default_layout_name - layout.is_a?(String) ? find_layout(layout, formats) : layout - rescue ActionView::MissingTemplate - end - end - - def default_layout(*args) - memoized_default_layout(*args) - @_memoized_default_layout ||= {} - @_memoized_default_layout[args] ||= memoized_default_layout(*args) - end - - def memoized_find_layout(layout, formats) #:nodoc: - return layout if layout.nil? || layout.respond_to?(:render) - prefix = layout.to_s =~ /layouts\// ? nil : "layouts" - find_template(layout.to_s, {:formats => formats}, :_prefix => prefix) - end - - def find_layout(*args) - @_memoized_find_layout ||= {} - @_memoized_find_layout[args] ||= memoized_find_layout(*args) - end - - def layout_list #:nodoc: - Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } - end - memoize :layout_list - - def default_layout_name - layout_match = name.underscore.sub(/_controller$/, '') - if layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty? - superclass.default_layout_name if superclass.respond_to?(:default_layout_name) - else - layout_match - end - end - memoize :default_layout_name - - private - def add_layout_conditions(conditions) - # :except => :foo == :except => [:foo] == :except => "foo" == :except => ["foo"] - conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } - write_inheritable_hash(:layout_conditions, conditions) - end - end - - def active_layout(name) - name = self.class.default_layout(formats) if name == true - - layout_name = case name - when Symbol then __send__(name) - when Proc then name.call(self) - else name - end - - self.class.find_layout(layout_name, formats) - end - - def _pick_layout(layout_name = nil, implicit = false) - return unless layout_name || implicit - layout_name = true if layout_name.nil? - active_layout(layout_name) if action_has_layout? && layout_name - end - - private - def action_has_layout? - if conditions = self.class.layout_conditions - if only = conditions[:only] - return only.include?(action_name) - elsif except = conditions[:except] - return !except.include?(action_name) - end - end - true - end - - end -end diff --git a/actionpack/lib/action_controller/metal/layouts.rb b/actionpack/lib/action_controller/metal/layouts.rb index cac529b1ae..e7859e3fec 100644 --- a/actionpack/lib/action_controller/metal/layouts.rb +++ b/actionpack/lib/action_controller/metal/layouts.rb @@ -177,16 +177,5 @@ module ActionController options[:_layout] = _layout_for_option(layout, options[:_template].details) end - def _layout_for_option(name, details) - case name - when String then _layout_for_name(name, details) - when true then _default_layout(details, true) - when :default then _default_layout(details, false) - when false, nil then nil - else - raise ArgumentError, - "String, true, or false, expected for `layout'; you passed #{name.inspect}" - end - end end end -- cgit v1.2.3 From 684c2dc20801b7fcc941ec9478d33d3bf7c74551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 18 Oct 2009 20:20:14 -0200 Subject: Remove ActionMailer helpers and rely on AbstractController one. --- actionpack/lib/abstract_controller/helpers.rb | 81 ++++++++++++++++++++--- actionpack/lib/action_controller/metal/helpers.rb | 69 ++----------------- actionpack/lib/action_view/render/rendering.rb | 2 +- 3 files changed, 75 insertions(+), 77 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index f3072fad74..d3b492ad09 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -1,3 +1,5 @@ +require 'active_support/dependencies' + module AbstractController module Helpers extend ActiveSupport::Concern @@ -57,23 +59,48 @@ module AbstractController end end - # Make a number of helper modules part of this class' default - # helpers. + # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Parameters - # *args:: Modules to be included - # block:: Evalulate the block in the context - # of the helper module. Any methods defined in the block - # will be helpers. + # *args + # block:: A block defining helper methods + # + # ==== Examples + # When the argument is a module it will be included directly in the template class. + # helper FooHelper # => includes FooHelper + # + # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file + # and include the module in the template class. The second form illustrates how to include custom helpers + # when working with namespaced controllers, or other cases where the file containing the helper definition is not + # in one of Rails' standard load paths: + # helper :foo # => requires 'foo_helper' and includes FooHelper + # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper + # + # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available + # to the template. + # + # # One line + # helper { def hello() "Hello, world!" end } + # + # # Multi-line + # helper do + # def foo(bar) + # "#{bar} is the very best" + # end + # end + # + # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of + # +symbols+, +strings+, +modules+ and blocks. + # + # helper(:three, BlindHelper) { def mice() 'mice' end } + # def helper(*args, &block) self._helper_serial = AbstractController::Helpers.next_serial + 1 - args.flatten.each do |arg| - case arg - when Module - add_template_helper(arg) - end + _modules_for_helpers(args).each do |mod| + add_template_helper(mod) end + _helpers.module_eval(&block) if block_given? end @@ -87,6 +114,38 @@ module AbstractController def add_template_helper(mod) _helpers.module_eval { include mod } end + + # Returns a list of modules, normalized from the acceptable kinds of + # helpers with the following behavior: + # + # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", + # and "foo_bar_helper.rb" is loaded using require_dependency. + # + # Module:: No further processing + # + # After loading the appropriate files, the corresponding modules + # are returned. + # + # ==== Parameters + # args:: A list of helpers + # + # ==== Returns + # Array[Module]:: A normalized list of modules for the list of + # helpers provided. + def _modules_for_helpers(args) + args.flatten.map! do |arg| + case arg + when String, Symbol + file_name = "#{arg.to_s.underscore}_helper" + require_dependency(file_name, "Missing helper file helpers/%s.rb") + file_name.camelize.constantize + when Module + arg + else + raise ArgumentError, "helper must be a String, Symbol, or Module" + end + end + end end end end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 117ea3481f..b4325e24ad 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,5 +1,3 @@ -require 'active_support/dependencies' - module ActionController # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+, # +numbers+ and model objects, to name a few. These helpers are available to all templates @@ -64,46 +62,6 @@ module ActionController super end - # The +helper+ class method can take a series of helper module names, a block, or both. - # - # ==== Parameters - # *args - # block:: A block defining helper methods - # - # ==== Examples - # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file - # and include the module in the template class. The second form illustrates how to include custom helpers - # when working with namespaced controllers, or other cases where the file containing the helper definition is not - # in one of Rails' standard load paths: - # helper :foo # => requires 'foo_helper' and includes FooHelper - # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper - # - # When the argument is a module it will be included directly in the template class. - # helper FooHelper # => includes FooHelper - # - # When the argument is the symbol :all, the controller will include all helpers beneath - # ActionController::Base.helpers_dir (defaults to app/helpers/**/*.rb under RAILS_ROOT). - # helper :all - # - # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available - # to the template. - # # One line - # helper { def hello() "Hello, world!" end } - # # Multi-line - # helper do - # def foo(bar) - # "#{bar} is the very best" - # end - # end - # - # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of - # +symbols+, +strings+, +modules+ and blocks. - # helper(:three, BlindHelper) { def mice() 'mice' end } - # - def helper(*args, &block) - super(*_modules_for_helpers(args), &block) - end - # Declares helper accessors for controller attributes. For example, the # following adds new +name+ and name= instance methods to a # controller and makes them available to the view: @@ -123,15 +81,8 @@ module ActionController end private - # Returns a list of modules, normalized from the acceptable kinds of - # helpers with the following behavior: - # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", - # and "foo_bar_helper.rb" is loaded using require_dependency. - # :all:: Loads all modules in the #helpers_dir - # Module:: No further processing - # - # After loading the appropriate files, the corresponding modules - # are returned. + # Overwrite _modules_for_helpers to accept :all as argument, which loads + # all helpers in helpers_dir. # # ==== Parameters # args:: A list of helpers @@ -140,20 +91,8 @@ module ActionController # Array[Module]:: A normalized list of modules for the list of # helpers provided. def _modules_for_helpers(args) - args.flatten.map! do |arg| - case arg - when :all - _modules_for_helpers all_application_helpers - when String, Symbol - file_name = "#{arg.to_s.underscore}_helper" - require_dependency(file_name, "Missing helper file helpers/%s.rb") - file_name.camelize.constantize - when Module - arg - else - raise ArgumentError, "helper must be a String, Symbol, or Module" - end - end + args += all_application_helpers if args.delete(:all) + super(args) end def default_helper_module! diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index 7dcd9d38d9..fc89726670 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -133,4 +133,4 @@ module ActionView content end end -end \ No newline at end of file +end -- cgit v1.2.3 From 43d5504f0a6a831474d149aa5f1ebb2545790152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 18 Oct 2009 22:52:36 -0200 Subject: Move all render and layout pieces required in ActionMailer from ActionController to AbstractController. --- actionpack/lib/abstract_controller/layouts.rb | 9 ++++++++ .../abstract_controller/rendering_controller.rb | 19 +++++++++++++++++ actionpack/lib/action_controller/metal/layouts.rb | 10 --------- .../metal/rendering_controller.rb | 24 ---------------------- actionpack/test/abstract/helper_test.rb | 4 +++- 5 files changed, 31 insertions(+), 35 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 4723e18a01..f4f1d41360 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -157,6 +157,7 @@ module AbstractController end private + # This will be overwritten by _write_layout_method def _layout(details) end @@ -191,6 +192,14 @@ module AbstractController end end + def _determine_template(options) + super + + return if (options.key?(:text) || options.key?(:inline) || options.key?(:partial)) && !options.key?(:layout) + layout = options.key?(:layout) ? options[:layout] : :default + options[:_layout] = _layout_for_option(layout, options[:_template].details) + end + # Take in the name and details and find a Template. # # ==== Parameters diff --git a/actionpack/lib/abstract_controller/rendering_controller.rb b/actionpack/lib/abstract_controller/rendering_controller.rb index bbf941aa32..9fb7935ce4 100644 --- a/actionpack/lib/abstract_controller/rendering_controller.rb +++ b/actionpack/lib/abstract_controller/rendering_controller.rb @@ -109,6 +109,21 @@ module AbstractController # to a directory. # _partial:: Whether or not the file to look up is a partial def _determine_template(options) + if options.key?(:text) + options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) + elsif options.key?(:inline) + handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") + template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) + options[:_template] = template + elsif options.key?(:template) + options[:_template_name] = options[:template] + elsif options.key?(:file) + options[:_template_name] = options[:file] + elsif !options.key?(:partial) + options[:_template_name] ||= options[:action] + options[:_prefix] = _prefix + end + name = (options[:_template_name] || action_name).to_s options[:_template] ||= with_template_cache(name) do @@ -124,6 +139,10 @@ module AbstractController view_paths.exists?(name, details, options[:_prefix], options[:_partial]) end + def _prefix + self.class.name.underscore + end + def with_template_cache(name) yield end diff --git a/actionpack/lib/action_controller/metal/layouts.rb b/actionpack/lib/action_controller/metal/layouts.rb index e7859e3fec..cc7088248a 100644 --- a/actionpack/lib/action_controller/metal/layouts.rb +++ b/actionpack/lib/action_controller/metal/layouts.rb @@ -167,15 +167,5 @@ module ActionController controller_path end end - - private - def _determine_template(options) - super - - return if (options.key?(:text) || options.key?(:inline) || options.key?(:partial)) && !options.key?(:layout) - layout = options.key?(:layout) ? options[:layout] : :default - options[:_layout] = _layout_for_option(layout, options[:_template].details) - end - end end diff --git a/actionpack/lib/action_controller/metal/rendering_controller.rb b/actionpack/lib/action_controller/metal/rendering_controller.rb index 4da32ca1b3..afd484b0ec 100644 --- a/actionpack/lib/action_controller/metal/rendering_controller.rb +++ b/actionpack/lib/action_controller/metal/rendering_controller.rb @@ -38,11 +38,6 @@ module ActionController def process_action(*) self.formats = request.formats.map {|x| x.to_sym} - - super - end - - def _determine_template(*) super end @@ -74,25 +69,6 @@ module ActionController self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super end - def _determine_template(options) - if options.key?(:text) - options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) - elsif options.key?(:inline) - handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") - template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) - options[:_template] = template - elsif options.key?(:template) - options[:_template_name] = options[:template] - elsif options.key?(:file) - options[:_template_name] = options[:file] - elsif !options.key?(:partial) - options[:_template_name] = (options[:action] || action_name).to_s - options[:_prefix] = _prefix - end - - super - end - def _process_options(options) status, content_type, location = options.values_at(:status, :content_type, :location) self.status = status if status diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb index 5a363c9aa5..b6952d2758 100644 --- a/actionpack/test/abstract/helper_test.rb +++ b/actionpack/test/abstract/helper_test.rb @@ -7,10 +7,12 @@ module AbstractController include AbstractController::RenderingController include Helpers + def _prefix() end + def render(string) super(:_template_name => string) end - + append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) end -- cgit v1.2.3 From 2d514e5352d17c8c3958b26397f1c808c7fa0b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 21 Oct 2009 14:42:58 -0200 Subject: Move some of helper tests to AbstractController. --- actionpack/test/abstract/helper_test.rb | 65 ++++++++++++++++++++++++------- actionpack/test/controller/helper_test.rb | 34 ---------------- 2 files changed, 50 insertions(+), 49 deletions(-) (limited to 'actionpack') diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb index b6952d2758..6c28f01fa4 100644 --- a/actionpack/test/abstract/helper_test.rb +++ b/actionpack/test/abstract/helper_test.rb @@ -1,19 +1,17 @@ require 'abstract_unit' +ActionController::Base.helpers_dir = File.dirname(__FILE__) + '/../fixtures/helpers' + module AbstractController module Testing class ControllerWithHelpers < AbstractController::Base include AbstractController::RenderingController include Helpers - - def _prefix() end - def render(string) - super(:_template_name => string) + def with_module + render :inline => "Module <%= included_method %>" end - - append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) end module HelperyTest @@ -22,24 +20,61 @@ module AbstractController end end - class MyHelpers1 < ControllerWithHelpers + class AbstractHelpers < ControllerWithHelpers helper(HelperyTest) do def helpery_test "World" end end - - def index - render "helper_test.erb" + + helper :abc + + def with_block + render :inline => "Hello <%= helpery_test %>" + end + + def with_symbol + render :inline => "I respond to bare_a: <%= respond_to?(:bare_a) %>" + end + end + + class AbstractHelpersBlock < ControllerWithHelpers + helper do + include HelperyTest end end - + class TestHelpers < ActiveSupport::TestCase - def test_helpers - controller = MyHelpers1.new - controller.process(:index) - assert_equal "Hello World : Included", controller.response_body + + def setup + @controller = AbstractHelpers.new end + + def test_helpers_with_block + @controller.process(:with_block) + assert_equal "Hello World", @controller.response_body + end + + def test_helpers_with_module + @controller.process(:with_module) + assert_equal "Module Included", @controller.response_body + end + + def test_helpers_with_symbol + @controller.process(:with_symbol) + assert_equal "I respond to bare_a: true", @controller.response_body + end + + def test_declare_missing_helper + assert_raise(MissingSourceFile) { AbstractHelpers.helper :missing } + end + + def test_helpers_with_module_through_block + @controller = AbstractHelpersBlock.new + @controller.process(:with_module) + assert_equal "Module Included", @controller.response_body + end + end end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 23149fee27..12539739aa 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -57,40 +57,6 @@ class HelperTest < Test::Unit::TestCase assert_equal [], missing_methods end - def test_declare_helper - require 'abc_helper' - self.test_helper = AbcHelper - assert_equal expected_helper_methods, missing_methods - assert_nothing_raised { @controller_class.helper :abc } - assert_equal [], missing_methods - end - - def test_declare_missing_helper - assert_equal expected_helper_methods, missing_methods - assert_raise(MissingSourceFile) { @controller_class.helper :missing } - end - - def test_declare_missing_file_from_helper - require 'broken_helper' - rescue LoadError => e - assert_nil(/\bbroken_helper\b/.match(e.to_s)[1]) - end - - def test_helper_block - assert_nothing_raised { - @controller_class.helper { def block_helper_method; end } - } - assert master_helper_methods.include?('block_helper_method') - end - - def test_helper_block_include - assert_equal expected_helper_methods, missing_methods - assert_nothing_raised { - @controller_class.helper { include HelperTest::TestHelper } - } - assert [], missing_methods - end - def test_helper_method assert_nothing_raised { @controller_class.helper_method :delegate_method } assert master_helper_methods.include?('delegate_method') -- cgit v1.2.3 From 0cf16ddb88b4fa28c37e576d50d835b100c3f6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 21 Oct 2009 15:55:47 -0200 Subject: Improve AbstractController layouts coverage. --- .../abstract_controller/rendering_controller.rb | 9 ++-- .../metal/rendering_controller.rb | 4 ++ actionpack/lib/action_view/template/text.rb | 4 +- actionpack/test/abstract/layouts_test.rb | 53 +++++++++++++++++----- 4 files changed, 53 insertions(+), 17 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/rendering_controller.rb b/actionpack/lib/abstract_controller/rendering_controller.rb index 9fb7935ce4..07deda77a2 100644 --- a/actionpack/lib/abstract_controller/rendering_controller.rb +++ b/actionpack/lib/abstract_controller/rendering_controller.rb @@ -8,9 +8,7 @@ module AbstractController included do attr_internal :formats - extlib_inheritable_accessor :_view_paths - self._view_paths ||= ActionView::PathSet.new end @@ -99,6 +97,7 @@ module AbstractController end private + # Take in a set of options and determine the template to render # # ==== Options @@ -110,7 +109,7 @@ module AbstractController # _partial:: Whether or not the file to look up is a partial def _determine_template(options) if options.key?(:text) - options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) + options[:_template] = ActionView::TextTemplate.new(options[:text], format_for_text) elsif options.key?(:inline) handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) @@ -147,6 +146,10 @@ module AbstractController yield end + def format_for_text + Mime[:text] + end + module ClassMethods def clear_template_caches! end diff --git a/actionpack/lib/action_controller/metal/rendering_controller.rb b/actionpack/lib/action_controller/metal/rendering_controller.rb index afd484b0ec..9e8bc82385 100644 --- a/actionpack/lib/action_controller/metal/rendering_controller.rb +++ b/actionpack/lib/action_controller/metal/rendering_controller.rb @@ -65,6 +65,10 @@ module ActionController controller_path end + def format_for_text + formats.first + end + def with_template_cache(name) self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super end diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index fa8cfe506b..f6e011a5ab 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -16,11 +16,11 @@ module ActionView #:nodoc: end def inspect - 'inline template' + 'text template' end def render(*args) - self + to_s end def mime_type diff --git a/actionpack/test/abstract/layouts_test.rb b/actionpack/test/abstract/layouts_test.rb index 453d31826e..9c29696ad5 100644 --- a/actionpack/test/abstract/layouts_test.rb +++ b/actionpack/test/abstract/layouts_test.rb @@ -17,17 +17,6 @@ module AbstractControllerTests "layouts/omg.erb" => "OMGHI2U <%= yield %>", "layouts/with_false_layout.erb" => "False Layout <%= yield %>" )] - - def self.controller_path - @controller_path ||= self.name.sub(/Controller$/, '').underscore - end - - def controller_path() self.class.controller_path end - - def render_to_body(options) - options[:_layout] = _default_layout({}) - super - end end class Blank < Base @@ -44,6 +33,22 @@ module AbstractControllerTests def index render :_template => ActionView::TextTemplate.new("Hello string!") end + + def overwrite_default + render :_template => ActionView::TextTemplate.new("Hello string!"), :layout => :default + end + + def overwrite_false + render :_template => ActionView::TextTemplate.new("Hello string!"), :layout => false + end + + def overwrite_string + render :_template => ActionView::TextTemplate.new("Hello string!"), :layout => "omg" + end + + def overwrite_skip + render :text => "Hello text!" + end end class WithStringChild < WithString @@ -153,7 +158,31 @@ module AbstractControllerTests controller.process(:index) assert_equal "With String Hello string!", controller.response_body end - + + test "when layout is overwriten by :default in render, render default layout" do + controller = WithString.new + controller.process(:overwrite_default) + assert_equal "With String Hello string!", controller.response_body + end + + test "when layout is overwriten by string in render, render new layout" do + controller = WithString.new + controller.process(:overwrite_string) + assert_equal "OMGHI2U Hello string!", controller.response_body + end + + test "when layout is overwriten by false in render, render no layout" do + controller = WithString.new + controller.process(:overwrite_false) + assert_equal "Hello string!", controller.response_body + end + + test "when text is rendered, render no layout" do + controller = WithString.new + controller.process(:overwrite_skip) + assert_equal "Hello text!", controller.response_body + end + test "when layout is specified as a string, but the layout is missing, raise an exception" do assert_raises(ActionView::MissingTemplate) { WithMissingLayout.new.process(:index) } end -- cgit v1.2.3 From 03960048616593c249745d1e321dbcc7f0483c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 21 Oct 2009 17:47:10 -0200 Subject: Add some basic render_test to AbstractController. --- actionpack/lib/abstract_controller/layouts.rb | 2 +- .../abstract_controller/rendering_controller.rb | 7 -- .../metal/rendering_controller.rb | 9 +++ actionpack/test/abstract/render_test.rb | 88 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 actionpack/test/abstract/render_test.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index f4f1d41360..8293e79b0a 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -195,7 +195,7 @@ module AbstractController def _determine_template(options) super - return if (options.key?(:text) || options.key?(:inline) || options.key?(:partial)) && !options.key?(:layout) + return unless (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout) layout = options.key?(:layout) ? options[:layout] : :default options[:_layout] = _layout_for_option(layout, options[:_template].details) end diff --git a/actionpack/lib/abstract_controller/rendering_controller.rb b/actionpack/lib/abstract_controller/rendering_controller.rb index 07deda77a2..0aae2b18e9 100644 --- a/actionpack/lib/abstract_controller/rendering_controller.rb +++ b/actionpack/lib/abstract_controller/rendering_controller.rb @@ -118,9 +118,6 @@ module AbstractController options[:_template_name] = options[:template] elsif options.key?(:file) options[:_template_name] = options[:file] - elsif !options.key?(:partial) - options[:_template_name] ||= options[:action] - options[:_prefix] = _prefix end name = (options[:_template_name] || action_name).to_s @@ -138,10 +135,6 @@ module AbstractController view_paths.exists?(name, details, options[:_prefix], options[:_partial]) end - def _prefix - self.class.name.underscore - end - def with_template_cache(name) yield end diff --git a/actionpack/lib/action_controller/metal/rendering_controller.rb b/actionpack/lib/action_controller/metal/rendering_controller.rb index 9e8bc82385..c5ade26702 100644 --- a/actionpack/lib/action_controller/metal/rendering_controller.rb +++ b/actionpack/lib/action_controller/metal/rendering_controller.rb @@ -65,6 +65,15 @@ module ActionController controller_path end + def _determine_template(options) + if (options.keys & [:partial, :file, :template, :text, :inline]).empty? + options[:_template_name] ||= options[:action] + options[:_prefix] = _prefix + end + + super + end + def format_for_text formats.first end diff --git a/actionpack/test/abstract/render_test.rb b/actionpack/test/abstract/render_test.rb new file mode 100644 index 0000000000..45a4763fe4 --- /dev/null +++ b/actionpack/test/abstract/render_test.rb @@ -0,0 +1,88 @@ +require 'abstract_unit' + +module AbstractController + module Testing + + class ControllerRenderer < AbstractController::Base + include AbstractController::RenderingController + + self.view_paths = [ActionView::FixtureResolver.new( + "default.erb" => "With Default", + "template.erb" => "With Template", + "some/file.erb" => "With File", + "template_name.erb" => "With Template Name" + )] + + def template + render :template => "template" + end + + def file + render :file => "some/file" + end + + def inline + render :inline => "With <%= :Inline %>" + end + + def text + render :text => "With Text" + end + + def default + render + end + + def template_name + render :_template_name => :template_name + end + + def object + render :_template => ActionView::TextTemplate.new("With Object") + end + end + + class TestRenderer < ActiveSupport::TestCase + + def setup + @controller = ControllerRenderer.new + end + + def test_render_template + @controller.process(:template) + assert_equal "With Template", @controller.response_body + end + + def test_render_file + @controller.process(:file) + assert_equal "With File", @controller.response_body + end + + def test_render_inline + @controller.process(:inline) + assert_equal "With Inline", @controller.response_body + end + + def test_render_text + @controller.process(:text) + assert_equal "With Text", @controller.response_body + end + + def test_render_default + @controller.process(:default) + assert_equal "With Default", @controller.response_body + end + + def test_render_template_name + @controller.process(:template_name) + assert_equal "With Template Name", @controller.response_body + end + + def test_render_object + @controller.process(:object) + assert_equal "With Object", @controller.response_body + end + + end + end +end -- cgit v1.2.3 From 976c2647240fd40a2b706ab5e41856cd47e7b212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Oct 2009 10:33:05 -0200 Subject: Extracted localized_cache.rb from ActionController, added it to AbstractController and made ActionMailer use it. --- actionpack/lib/abstract_controller.rb | 1 + .../lib/abstract_controller/localized_cache.rb | 49 +++++++++++++++++++ .../metal/rendering_controller.rb | 41 ++-------------- actionpack/lib/action_view/base.rb | 4 +- actionpack/test/abstract/localized_cache_test.rb | 57 ++++++++++++++++++++++ 5 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 actionpack/lib/abstract_controller/localized_cache.rb create mode 100644 actionpack/test/abstract/localized_cache_test.rb (limited to 'actionpack') diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 76c5845f5b..1a6c4278c9 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -6,6 +6,7 @@ module AbstractController autoload :Callbacks, "abstract_controller/callbacks" autoload :Helpers, "abstract_controller/helpers" autoload :Layouts, "abstract_controller/layouts" + autoload :LocalizedCache, "abstract_controller/localized_cache" autoload :Logger, "abstract_controller/logger" autoload :RenderingController, "abstract_controller/rendering_controller" # === Exceptions diff --git a/actionpack/lib/abstract_controller/localized_cache.rb b/actionpack/lib/abstract_controller/localized_cache.rb new file mode 100644 index 0000000000..ee7b43cb9f --- /dev/null +++ b/actionpack/lib/abstract_controller/localized_cache.rb @@ -0,0 +1,49 @@ +module AbstractController + class HashKey + @hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } } + + def self.get(klass, formats, locale) + @hash_keys[klass][formats][locale] ||= new(klass, formats, locale) + end + + attr_accessor :hash + def initialize(klass, formats, locale) + @formats, @locale = formats, locale + @hash = [formats, locale].hash + end + + alias_method :eql?, :equal? + + def inspect + "#" + end + end + + module LocalizedCache + extend ActiveSupport::Concern + + module ClassMethods + def clear_template_caches! + ActionView::Partials::PartialRenderer::TEMPLATES.clear + template_cache.clear + super + end + + def template_cache + @template_cache ||= Hash.new {|h,k| h[k] = {} } + end + end + + def render(options) + Thread.current[:format_locale_key] = HashKey.get(self.class, formats, I18n.locale) + super + end + + private + + def with_template_cache(name) + self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super + end + + end +end diff --git a/actionpack/lib/action_controller/metal/rendering_controller.rb b/actionpack/lib/action_controller/metal/rendering_controller.rb index c5ade26702..237299cd30 100644 --- a/actionpack/lib/action_controller/metal/rendering_controller.rb +++ b/actionpack/lib/action_controller/metal/rendering_controller.rb @@ -1,39 +1,10 @@ module ActionController - class HashKey - @hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } } - - def self.get(klass, formats, locale) - @hash_keys[klass][formats][locale] ||= new(klass, formats, locale) - end - - attr_accessor :hash - def initialize(klass, formats, locale) - @formats, @locale = formats, locale - @hash = [formats, locale].hash - end - - alias_method :eql?, :equal? - - def inspect - "#" - end - end - module RenderingController extend ActiveSupport::Concern - include AbstractController::RenderingController - - module ClassMethods - def clear_template_caches! - ActionView::Partials::PartialRenderer::TEMPLATES.clear - template_cache.clear - super - end - - def template_cache - @template_cache ||= Hash.new {|h,k| h[k] = {} } - end + included do + include AbstractController::RenderingController + include AbstractController::LocalizedCache end def process_action(*) @@ -42,8 +13,6 @@ module ActionController end def render(options) - Thread.current[:format_locale_key] = HashKey.get(self.class, formats, I18n.locale) - super self.content_type ||= options[:_template].mime_type.to_s response_body @@ -78,10 +47,6 @@ module ActionController formats.first end - def with_template_cache(name) - self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super - end - def _process_options(options) status, content_type, location = options.values_at(:status, :content_type, :location) self.status = status if status diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 84e4103280..c33695770f 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -187,11 +187,11 @@ module ActionView #:nodoc: def reset_formats(formats) @formats = formats - if defined?(ActionController) + if defined?(AbstractController::HashKey) # This is expensive, but we need to reset this when the format is updated, # which currently only happens Thread.current[:format_locale_key] = - ActionController::HashKey.get(self.class, formats, I18n.locale) + AbstractController::HashKey.get(self.class, formats, I18n.locale) end end diff --git a/actionpack/test/abstract/localized_cache_test.rb b/actionpack/test/abstract/localized_cache_test.rb new file mode 100644 index 0000000000..6f9bb693f7 --- /dev/null +++ b/actionpack/test/abstract/localized_cache_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' + +module AbstractController + module Testing + + class CachedController < AbstractController::Base + include AbstractController::RenderingController + include AbstractController::LocalizedCache + + self.view_paths = [ActionView::FixtureResolver.new( + "default.erb" => "With Default", + "template.erb" => "With Template", + "some/file.erb" => "With File", + "template_name.erb" => "With Template Name" + )] + end + + class TestLocalizedCache < ActiveSupport::TestCase + + def setup + @controller = CachedController.new + CachedController.clear_template_caches! + end + + def test_templates_are_cached + @controller.render :template => "default.erb" + assert_equal "With Default", @controller.response_body + + cached = @controller.class.template_cache + assert_equal 1, cached.size + assert_kind_of ActionView::Template, cached.values.first["default.erb"] + end + + def test_cache_is_used + CachedController.new.render :template => "default.erb" + + @controller.expects(:find_template).never + @controller.render :template => "default.erb" + + assert_equal 1, @controller.class.template_cache.size + end + + def test_cache_changes_with_locale + CachedController.new.render :template => "default.erb" + + I18n.locale = :es + @controller.render :template => "default.erb" + + assert_equal 2, @controller.class.template_cache.size + ensure + I18n.locale = :en + end + + end + + end +end -- cgit v1.2.3