From 99e884bc137ef32664effe60ddf65a7d1f3acf45 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Tue, 1 Jul 2014 04:17:47 +0530 Subject: [PATCH 01/23] Create basic accessors for scope properties in AbstractScope --- .../routing/dsl/abstract_scope.rb | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb new file mode 100644 index 0000000000000..bc025d12b68ff --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -0,0 +1,78 @@ +module DSL + class AbstractScope + # Constants + # ========= + URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] + SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] + + # Accessors + # ========= + attr_accessor :parent + attr_reader :controller, :action + + def path + merge_with_slash(parent.path, @_path) + end + + def shallow_path + merge_with_slash(parent.shallow_path, @_shallow_path) + end + + def as + merge_with_underscore(parent.as, @_as) + end + + def shallow_prefix + merge_with_underscore(parent.shallow_prefix, @_shallow_prefix) + end + + def module + parent ? "#{parent.module}/#{@_module}" : @_module + end + + def path_names + merge_hashes(parent.path_names, @_path_names) + end + + def constraints + merge_hashes(parent.constraints, @_constraints) + end + + def shallow? + @_shallow + end + + def blocks + merged = parent.blocks ? parent.blocks.dup : [] + merged << @_blocks if @_blocks + merged + end + + def defaults + merge_hashes(parent.defaults, @_defaults) + end + + def options + merge_hashes(parent.options, @_options) + end + + protected + def merge_with_slash(parent, child) + Mapper.normalize_path("#{parent}/#{child}") + end + + def merge_with_underscore(parent, child) + parent ? "#{parent}_#{child}" : child + end + + def merge_hashes(parent, child) + (parent || {}).except(*override_keys(child)).merge!(child) + end + + def override_keys(child) #:nodoc: + child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + end + end +end From ce445251a4c8bc9702af18017bd3acd938ca7290 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Tue, 1 Jul 2014 07:22:52 +0530 Subject: [PATCH 02/23] Implemented initialization of Abstract Scope We set the options and then set all instance variables Added a nil refinement so that the system do not break with a nil parent --- .../routing/dsl/abstract_scope.rb | 41 ++++++++++++++++++- .../dsl/abstract_scope/nil_refinement.rb | 15 +++++++ .../dsl/abstract_scope/normalization.rb | 17 ++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index bc025d12b68ff..4e76336853edd 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -1,3 +1,7 @@ +require 'active_support/core_ext/array/extract_options' +require 'action_dispatch/routing/dsl/abstract_scope/nil_refinement' +require 'action_dispatch/routing/dsl/abstract_scope/normalization' + module DSL class AbstractScope # Constants @@ -10,7 +14,40 @@ class AbstractScope # Accessors # ========= attr_accessor :parent - attr_reader :controller, :action + attr_reader :controller, :action, :set, :concerns + + def initialize(parent, *args) + @parent, @set, @concerns = parent, parent.set, parent.concerns + + # Extract options out of the variable arguments + options = args.extract_options!.dup + + options[:path] = args.flatten.join('/') if args.any? + options[:constraints] ||= {} + + if options[:constraints].is_a?(Hash) + defaults = options[:constraints].select do + |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + end + + (options[:defaults] ||= {}).reverse_merge!(defaults) + else + block, options[:constraints] = options[:constraints], {} + end + + SCOPE_OPTIONS.each do |option| + if option == :blocks + value = block + elsif option == :options + value = options + else + value = options.delete(option) + end + + # Set instance variables + instance_variable_set(:"@_#{option}", value) if value + end + end def path merge_with_slash(parent.path, @_path) @@ -60,7 +97,7 @@ def options protected def merge_with_slash(parent, child) - Mapper.normalize_path("#{parent}/#{child}") + normalize_path("#{parent}/#{child}") end def merge_with_underscore(parent, child) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb new file mode 100644 index 0000000000000..5c4c3455d25fa --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb @@ -0,0 +1,15 @@ +module DSL + refine NilClass do + def concerns; {} ; end + + # Alias all these methods to me and return nil + [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options].each { |option| alias option, :ujjwal } + + alias :set, :ujjwal + + private + def ujjwal; nil ; end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb new file mode 100644 index 0000000000000..658fc60ddb8b6 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb @@ -0,0 +1,17 @@ +module DSL + class AbstractScope + protected + # Invokes Journey::Router::Utils.normalize_path and ensure that + # (:locale) becomes (/:locale) instead of /(:locale). Except + # for root cases, where the latter is the correct one. + def normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path + end + + def normalize_name(name) + normalize_path(name)[1..-1].tr("/", "_") + end + end +end From 56baf07c82886910cf3ec1bab7873f7525503f0b Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Tue, 1 Jul 2014 11:11:17 +0530 Subject: [PATCH 03/23] Remove NilClass refinements as they're not supported on JRuby --- .../action_dispatch/routing/dsl/abstract_scope.rb | 1 - .../routing/dsl/abstract_scope/nil_refinement.rb | 15 --------------- 2 files changed, 16 deletions(-) delete mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 4e76336853edd..02025d581c705 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/extract_options' -require 'action_dispatch/routing/dsl/abstract_scope/nil_refinement' require 'action_dispatch/routing/dsl/abstract_scope/normalization' module DSL diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb deleted file mode 100644 index 5c4c3455d25fa..0000000000000 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/nil_refinement.rb +++ /dev/null @@ -1,15 +0,0 @@ -module DSL - refine NilClass do - def concerns; {} ; end - - # Alias all these methods to me and return nil - [:path, :shallow_path, :as, :shallow_prefix, :module, - :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options].each { |option| alias option, :ujjwal } - - alias :set, :ujjwal - - private - def ujjwal; nil ; end - end -end From 4ffcba0d71f040aa90f4c7a771c54a86c4dbd720 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Tue, 1 Jul 2014 23:28:56 +0530 Subject: [PATCH 04/23] Refactored match and http helpers and mount for generating routes --- .../routing/dsl/abstract_scope.rb | 205 ++++++----- .../dsl/abstract_scope/http_helpers.rb | 55 +++ .../routing/dsl/abstract_scope/mapping.rb | 340 ++++++++++++++++++ .../routing/dsl/abstract_scope/match.rb | 259 +++++++++++++ .../routing/dsl/abstract_scope/mount.rb | 132 +++++++ .../dsl/abstract_scope/normalization.rb | 32 +- .../lib/action_dispatch/routing/dsl/scope.rb | 10 + .../lib/action_dispatch/routing/route_set.rb | 11 +- 8 files changed, 932 insertions(+), 112 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/scope.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 02025d581c705..43b85cbe3c705 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -1,114 +1,133 @@ -require 'active_support/core_ext/array/extract_options' require 'action_dispatch/routing/dsl/abstract_scope/normalization' - -module DSL - class AbstractScope - # Constants - # ========= - URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, - :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] - - # Accessors - # ========= - attr_accessor :parent - attr_reader :controller, :action, :set, :concerns - - def initialize(parent, *args) - @parent, @set, @concerns = parent, parent.set, parent.concerns - - # Extract options out of the variable arguments - options = args.extract_options!.dup - - options[:path] = args.flatten.join('/') if args.any? - options[:constraints] ||= {} - - if options[:constraints].is_a?(Hash) - defaults = options[:constraints].select do - |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) +require 'action_dispatch/routing/dsl/abstract_scope/mount' +require 'action_dispatch/routing/dsl/abstract_scope/match' +require 'action_dispatch/routing/dsl/abstract_scope/http_helpers' + +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Constants + # ========= + URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] + SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] + + # Accessors + # ========= + attr_accessor :set + attr_reader :controller, :action + + def initialize(parent, *args) + if parent + @parent, @set, @concerns = parent, parent.set, parent.concerns + else + @parent, @concerns = nil, {} + end + + # Extract options out of the variable arguments + options = args.extract_options!.dup + + options[:path] = args.flatten.join('/') if args.any? + options[:constraints] ||= {} + + if options[:constraints].is_a?(Hash) + defaults = options[:constraints].select do + |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + end + + (options[:defaults] ||= {}).reverse_merge!(defaults) + else + block, options[:constraints] = options[:constraints], {} + end + + SCOPE_OPTIONS.each do |option| + if option == :blocks + value = block + elsif option == :options + value = options + else + value = options.delete(option) + end + + # Set instance variables + instance_variable_set(:"@#{option}", value) if value + end end - (options[:defaults] ||= {}).reverse_merge!(defaults) - else - block, options[:constraints] = options[:constraints], {} - end - - SCOPE_OPTIONS.each do |option| - if option == :blocks - value = block - elsif option == :options - value = options - else - value = options.delete(option) + def path + parent_path = parent ? parent.path : nil + merge_with_slash(parent_path, @path) end - # Set instance variables - instance_variable_set(:"@_#{option}", value) if value - end - end - - def path - merge_with_slash(parent.path, @_path) - end - - def shallow_path - merge_with_slash(parent.shallow_path, @_shallow_path) - end + def shallow_path + parent_shallow_path = parent ? parent.shallow_path : nil + merge_with_slash(parent_shallow_path, @shallow_path) + end - def as - merge_with_underscore(parent.as, @_as) - end + def as + parent_as = parent ? parent.as : nil + merge_with_underscore(parent_as, @as) + end - def shallow_prefix - merge_with_underscore(parent.shallow_prefix, @_shallow_prefix) - end + def shallow_prefix + parent_shallow_prefix = parent ? parent.shallow_prefix : nil + merge_with_underscore(parent_shallow_prefix, @shallow_prefix) + end - def module - parent ? "#{parent.module}/#{@_module}" : @_module - end + def module + parent ? "#{parent.module}/#{@module}" : @module + end - def path_names - merge_hashes(parent.path_names, @_path_names) - end + def path_names + parent_path_names = parent ? parent.path_names : nil + merge_hashes(parent_path_names, @path_names) + end - def constraints - merge_hashes(parent.constraints, @_constraints) - end + def constraints + parent_constraints = parent ? parent.constraints : nil + merge_hashes(parent_constraints, @constraints) + end - def shallow? - @_shallow - end + def shallow? + @shallow + end - def blocks - merged = parent.blocks ? parent.blocks.dup : [] - merged << @_blocks if @_blocks - merged - end + def blocks + parent_blocks = parent ? parent.blocks : nil + merged = parent_blocks ? parent_blocks.dup : [] + merged << @blocks if @blocks + merged + end - def defaults - merge_hashes(parent.defaults, @_defaults) - end + def defaults + parent_defaults = parent ? parent.defaults : nil + merge_hashes(parent_defaults, @defaults) + end - def options - merge_hashes(parent.options, @_options) - end + def options + parent_options = parent ? parent.options : nil + merge_hashes(parent_options, @options) + end - protected - def merge_with_slash(parent, child) - normalize_path("#{parent}/#{child}") - end + protected + def merge_with_slash(parent, child) + normalize_path("#{parent}/#{child}") + end - def merge_with_underscore(parent, child) - parent ? "#{parent}_#{child}" : child - end + def merge_with_underscore(parent, child) + parent ? "#{parent}_#{child}" : child + end - def merge_hashes(parent, child) - (parent || {}).except(*override_keys(child)).merge!(child) - end + def merge_hashes(parent, child) + (parent || {}).except(*override_keys(child)).merge!(child) + end - def override_keys(child) #:nodoc: - child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + def override_keys(child) #:nodoc: + child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + end end + end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb new file mode 100644 index 0000000000000..a79e9803e74bf --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb @@ -0,0 +1,55 @@ +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Define a route that only recognizes HTTP GET. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # get 'bacon', to: 'food#bacon' + def get(*args, &block) + map_method(:get, args, &block) + end + + # Define a route that only recognizes HTTP POST. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # post 'bacon', to: 'food#bacon' + def post(*args, &block) + map_method(:post, args, &block) + end + + # Define a route that only recognizes HTTP PATCH. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # patch 'bacon', to: 'food#bacon' + def patch(*args, &block) + map_method(:patch, args, &block) + end + + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # put 'bacon', to: 'food#bacon' + def put(*args, &block) + map_method(:put, args, &block) + end + + # Define a route that only recognizes HTTP DELETE. + # For supported arguments, see match[rdoc-ref:Base#match] + # + # delete 'broccoli', to: 'food#broccoli' + def delete(*args, &block) + map_method(:delete, args, &block) + end + + protected + def map_method(method, args, &block) + options = args.extract_options! + options[:via] = method + match(*args, options, &block) + self + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb new file mode 100644 index 0000000000000..43893641b1bbe --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb @@ -0,0 +1,340 @@ +module ActionDispatch + module Routing + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app + end + + @dispatcher = dispatcher_p + + @app, @constraints, = app, constraints + end + + def dispatcher?; @dispatcher; end + + def matches?(req) + @constraints.all? do |constraint| + (constraint.respond_to?(:matches?) && constraint.matches?(req)) || + (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) + end + end + + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end + end + + private + def constraint_args(constraint, request) + constraint.arity == 1 ? [request] : [request.path_parameters, request] + end + end + + + class Mapping #:nodoc: + ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + + def self.build(scope, path, options) + options = scope.options.merge(options) if scope.options + + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow + + defaults = (scope.defaults || {}).merge options.delete(:defaults) || {} + + new scope, path, defaults, options + end + + def initialize(scope, path, defaults, options) + @requirements, @conditions = {}, {} + @defaults = defaults + + @to = options.delete :to + @default_controller = options.delete(:controller) || scope.controller + @default_action = options.delete(:action) || scope.action + @as = options.delete :as + @anchor = options.delete :anchor + + formatted = options.delete :format + via = Array(options.delete(:via) { [] }) + options_constraints = options.delete :constraints + + path = normalize_path! path, formatted + ast = path_ast path + path_params = path_params ast + + options = normalize_options!(options, formatted, path_params, ast, scope.module) + + + split_constraints(path_params, scope.constraints) if scope.constraints + constraints = constraints(options, path_params) + + split_constraints path_params, constraints + + @blocks = blocks(options_constraints, scope.blocks) + + if options_constraints.is_a?(Hash) + split_constraints path_params, options_constraints + options_constraints.each do |key, default| + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end + end + + normalize_format!(formatted) + + @conditions[:path_info] = path + @conditions[:parsed_path_info] = ast + + add_request_method(via, @conditions) + normalize_defaults!(options) + end + + def to_route + [ app(@blocks), conditions, requirements, defaults, as, anchor ] + end + + private + + def normalize_path!(path, format) + path = Mapper.normalize_path(path) + + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end + + def optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') + end + + def normalize_options!(options, formatted, path_params, path_ast, modyoule) + # Add a constraint for wildcard route to make it non-greedy and match the + # optional format part of the route by default + if formatted != false + path_ast.grep(Journey::Nodes::Star) do |node| + options[node.name.to_sym] ||= /.+?/ + end + end + + if path_params.include?(:controller) + raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { controller: 'admin/products', action: 'show', id: '1' } + options[:controller] ||= /.+?/ + end + + if to.respond_to? :call + options + else + to_endpoint = split_to to + controller = to_endpoint[0] || default_controller + action = to_endpoint[1] || default_action + + controller = add_controller_module(controller, modyoule) + + options.merge! check_controller_and_action(path_params, controller, action) + end + end + + def split_constraints(path_params, constraints) + constraints.each_pair do |key, requirement| + if path_params.include?(key) || key == :controller + verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) + @requirements[key] = requirement + else + @conditions[key] = requirement + end + end + end + + def normalize_format!(formatted) + if formatted == true + @requirements[:format] ||= /.+/ + elsif Regexp === formatted + @requirements[:format] = formatted + @defaults[:format] = nil + elsif String === formatted + @requirements[:format] = Regexp.compile(formatted) + @defaults[:format] = formatted + end + end + + def verify_regexp_requirement(requirement) + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + end + end + + def normalize_defaults!(options) + options.each_pair do |key, default| + unless Regexp === default + @defaults[key] = default + end + end + end + + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + end + end + + def add_request_method(via, conditions) + return if via == [:all] + + if via.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + + conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + end + + def app(blocks) + return to if Redirect === to + + if to.respond_to?(:call) + Constraints.new(to, blocks, false) + else + if blocks.any? + Constraints.new(dispatcher, blocks, true) + else + dispatcher + end + end + end + + def check_controller_and_action(path_params, controller, action) + hash = check_part(:controller, controller, path_params, {}) do |part| + translate_controller(part) { + message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + + raise ArgumentError, message + } + end + + check_part(:action, action, path_params, hash) { |part| + part.is_a?(Regexp) ? part : part.to_s + } + end + + def check_part(name, part, path_params, hash) + if part + hash[name] = yield(part) + else + unless path_params.include?(name) + message = "Missing :#{name} key on routes definition, please check your routes." + raise ArgumentError, message + end + end + hash + end + + def split_to(to) + case to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] + else + [] + end + end + + def add_controller_module(controller, modyoule) + if modyoule && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller[1..-1] + else + [modyoule, controller].compact.join("/") + end + else + controller + end + end + + def translate_controller(controller) + return controller if Regexp === controller + return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/ + + yield + end + + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] + else + scope_blocks || [] + end + end + + def constraints(options, path_params) + constraints = {} + required_defaults = [] + options.each_pair do |key, option| + if Regexp === option + constraints[key] = option + else + required_defaults << key unless path_params.include?(key) + end + end + @conditions[:required_defaults] = required_defaults + constraints + end + + def path_params(ast) + ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } + end + + def path_ast(path) + parser = Journey::Parser.new + parser.parse path + end + + def dispatcher + Routing::RouteSet::Dispatcher.new(defaults) + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb new file mode 100644 index 0000000000000..b45f669e0a1b7 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb @@ -0,0 +1,259 @@ +require 'action_dispatch/routing/dsl/abstract_scope/mapping' + +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Matches a url pattern to one or more routes. + # + # You should not use the `match` method in your router + # without specifying an HTTP method. + # + # If you want to expose your action to both GET and POST, use: + # + # # sets :controller, :action and :id in params + # match ':controller/:action/:id', via: [:get, :post] + # + # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # query parameters and thus available through +params+ in an action. + # + # If you want to expose your action to GET, use `get` in the router: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" + # + # Two of these symbols are special, +:controller+ maps to the controller + # and +:action+ to the controller's action. A pattern can also map + # wildcard segments (globs) to params: + # + # get 'songs/*category/:title', to: 'songs#show' + # + # # 'songs/rock/classic/stairway-to-heaven' sets + # # params[:category] = 'rock/classic' + # # params[:title] = 'stairway-to-heaven' + # + # To match a wildcard parameter, it must have a name assigned to it. + # Without a variable name to attach the glob parameter to, the route + # can't be parsed. + # + # When a pattern points to an internal route, the route's +:action+ and + # +:controller+ should be set in options or hash shorthand. Examples: + # + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get + # + # A pattern can also point to a +Rack+ endpoint i.e. anything that + # responds to +call+: + # + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get + # # Yes, controller actions are just rack endpoints + # match 'photos/:id', to: PhotosController.action(:show), via: :get + # + # Because requesting various HTTP verbs with a single action has security + # implications, you must either specify the actions in + # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] + # instead +match+ + # + # === Options + # + # Any options not seen here are passed on as params with the url. + # + # [:controller] + # The route's controller. + # + # [:action] + # The route's action. + # + # [:param] + # Overrides the default resource identifier `:id` (name of the + # dynamic segment used to generate the routes). + # You can access that segment from your controller using + # params[<:param>]. + # + # [:path] + # The path prefix for the routes. + # + # [:module] + # The namespace for :controller. + # + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get + # # => Sekret::PostsController + # + # See Scoping#namespace for its scope equivalent. + # + # [:as] + # The name used to generate routing helpers. + # + # [:via] + # Allowed HTTP verb(s) for route. + # + # match 'path', to: 'c#a', via: :get + # match 'path', to: 'c#a', via: [:get, :post] + # match 'path', to: 'c#a', via: :all + # + # [:to] + # Points to a +Rack+ endpoint. Can be an object that responds to + # +call+ or a string representing a controller's action. + # + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get + # + # [:on] + # Shorthand for wrapping routes in a specific RESTful context. Valid + # values are +:member+, +:collection+, and +:new+. Only use within + # resource(s) block. For example: + # + # resource :bar do + # match 'foo', to: 'c#a', on: :member, via: [:get, :post] + # end + # + # Is equivalent to: + # + # resource :bar do + # member do + # match 'foo', to: 'c#a', via: [:get, :post] + # end + # end + # + # [:constraints] + # Constrains parameters with a hash of regular expressions + # or an object that responds to matches?. In addition, constraints + # other than path can also be specified with any object + # that responds to === (eg. String, Array, Range, etc.). + # + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get + # + # match 'json_only', constraints: { format: 'json' }, via: :get + # + # class Whitelist + # def matches?(request) request.remote_ip == '1.2.3.4' end + # end + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # + # See Scoping#constraints for more examples with its scope + # equivalent. + # + # [:defaults] + # Sets defaults for parameters + # + # # Sets params[:format] to 'jpg' by default + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get + # + # See Scoping#defaults for its scope equivalent. + # + # [:anchor] + # Boolean to anchor a match pattern. Default is true. When set to + # false, the pattern matches any request prefixed with the given path. + # + # # Matches any request starting with 'path' + # match 'path', to: 'c#a', anchor: false, via: :get + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. + def match(path, *rest) + if rest.empty? && Hash === path + options = path + path, to = options.find { |name, _value| name.is_a?(String) } + + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to + end + else + options[:to] = to + end + + options.delete(path) + paths = [path] + else + options = rest.pop || {} + paths = [path] + rest + end + + options[:anchor] = true unless options.key?(:anchor) + + if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) + raise ArgumentError, "Unknown scope #{on.inspect} given to :on" + end + + if controller && action + options[:to] ||= "#{controller}##{action}" + end + + paths.each do |_path| + route_options = options.dup + route_options[:path] ||= _path if _path.is_a?(String) + + path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format, route_options) + route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") + end + + decomposed_match(_path, route_options) + end + self + end + + def using_match_shorthand?(path, options) + path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$} + end + + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + add_route(path, options) + end + end + + def add_route(action, options) # :nodoc: + path = path_for_action(action, options.delete(:path)) + raise ArgumentError, "path is required" if path.blank? + + action = action.to_s.dup + + if action =~ /^[\w\-\/]+$/ + options[:action] ||= action.tr('-', '_') unless action.include?("/") + else + action = nil + end + + if !options.fetch(:as, true) + options.delete(:as) + else + options[:as] = name_for_action(options[:as], action) + end + + mapping = Mapping.build(self, URI.parser.escape(path), options) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) + end + + def path_for_action(action, path) #:nodoc: + "#{self.path}/#{action_path(action, path)}" + end + + def action_path(name, path = nil) #:nodoc: + name = name.to_sym if name.is_a?(String) + path || @scope[:path_names][name] || name.to_s + end + + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb new file mode 100644 index 0000000000000..5c6f925618dc6 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb @@ -0,0 +1,132 @@ +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, at: "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # For options, see +match+, as +mount+ uses it internally. + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", as: "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. + def mount(app, options = nil) + if options + path = options.delete(:at) + else + unless Hash === app + raise ArgumentError, "must be called with mount point" + end + + options = app + app, path = options.find { |k, _| k.respond_to?(:call) } + options.delete(app) if app + end + + raise "A rack application must be specified" unless path + + options[:as] ||= app_name(app) + target_as = name_for_action(options[:as], path) + options[:via] ||= :all + + match(path, options.merge(:to => app, :anchor => false, :format => false)) + + define_generate_prefix(app, target_as) + self + end + + protected + def app_name(app) + return unless app.respond_to?(:routes) + + if app.respond_to?(:railtie_name) + app.railtie_name + else + class_name = app.class.is_a?(Class) ? app.name : app.class.name + ActiveSupport::Inflector.underscore(class_name).tr("/", "_") + end + end + + def define_generate_prefix(app, name) + return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) + + _route = @set.named_routes.routes[name.to_sym] + _routes = @set + app.routes.define_mounted_helper(name) + app.routes.extend Module.new { + def mounted?; true; end + define_method :find_script_name do |options| + super(options) || begin + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) + end + end + } + end + + def prefix_name_for_action(as, action) #:nodoc: + if as + prefix = as + # elsif !canonical_action?(action, @scope[:scope_level]) + # prefix = action + end + prefix.to_s.tr('-', '_') if prefix + end + + def name_for_action(as, action) #:nodoc: + prefix = prefix_name_for_action(as, action) + prefix = normalize_name(prefix) if prefix + name_prefix = self.as + + # if parent_resource + # return nil unless as || action + + # collection_name = parent_resource.collection_name + # member_name = parent_resource.member_name + # end + + # name = case @scope[:scope_level] + # when :nested + # [name_prefix, prefix] + # when :collection + # [prefix, name_prefix, collection_name] + # when :new + # [prefix, :new, name_prefix, member_name] + # when :member + # [prefix, name_prefix, member_name] + # when :root + # [name_prefix, collection_name, prefix] + # else + # [name_prefix, member_name, prefix] + # end + + name = [name_prefix, prefix] + + if candidate = name.select(&:present?).join("_").presence + # If a name was not explicitly given, we check if it is valid + # and return nil in case it isn't. Otherwise, we pass the invalid name + # forward so the underlying router engine treats it and raises an exception. + if as.nil? + candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + else + candidate + end + end + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb index 658fc60ddb8b6..d780751720ff6 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb @@ -1,17 +1,23 @@ -module DSL - class AbstractScope - protected - # Invokes Journey::Router::Utils.normalize_path and ensure that - # (:locale) becomes (/:locale) instead of /(:locale). Except - # for root cases, where the latter is the correct one. - def normalize_path(path) - path = Journey::Router::Utils.normalize_path(path) - path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} - path - end +require 'action_dispatch/journey' + +module ActionDispatch + module Routing + module DSL + class AbstractScope + protected + # Invokes Journey::Router::Utils.normalize_path and ensure that + # (:locale) becomes (/:locale) instead of /(:locale). Except + # for root cases, where the latter is the correct one. + def normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path + end - def normalize_name(name) - normalize_path(name)[1..-1].tr("/", "_") + def normalize_name(name) + normalize_path(name)[1..-1].tr("/", "_") + end + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb new file mode 100644 index 0000000000000..b1e28de1570b5 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -0,0 +1,10 @@ +require 'action_dispatch/routing/dsl/abstract_scope' + +module ActionDispatch + module Routing + module DSL + class Scope < AbstractScope + 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 69535faabd78c..468c0403af26a 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -9,6 +9,7 @@ require 'action_controller/metal/exceptions' require 'action_dispatch/http/request' require 'action_dispatch/routing/endpoint' +require 'action_dispatch/routing/dsl/scope' module ActionDispatch module Routing @@ -312,12 +313,10 @@ def eval_block(block) raise "You are using the old router DSL which has been removed in Rails 3.1. " << "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" end - mapper = Mapper.new(self) - if default_scope - mapper.with_default_scope(default_scope, &block) - else - mapper.instance_exec(&block) - end + scope_defaults = default_scope || { :path_names => resources_path_names } + scope = DSL::Scope.new(nil, scope_defaults) + scope.set = self + scope.instance_exec(&block) end def finalize! From 5e28c7693fc4e04f40f5b16a7001ebd89f9a82d5 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 04:29:29 +0530 Subject: [PATCH 05/23] Fixed bug in Scope.module where empty module rendered as '/' Added scoping methods --- .../routing/dsl/abstract_scope.rb | 6 +- .../routing/dsl/abstract_scope/scoping.rb | 230 ++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 43b85cbe3c705..35ef509165662 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -2,6 +2,7 @@ require 'action_dispatch/routing/dsl/abstract_scope/mount' require 'action_dispatch/routing/dsl/abstract_scope/match' require 'action_dispatch/routing/dsl/abstract_scope/http_helpers' +require 'action_dispatch/routing/dsl/abstract_scope/scoping' module ActionDispatch module Routing @@ -16,7 +17,7 @@ class AbstractScope # Accessors # ========= - attr_accessor :set + attr_accessor :parent, :set, :concerns attr_reader :controller, :action def initialize(parent, *args) @@ -77,7 +78,8 @@ def shallow_prefix end def module - parent ? "#{parent.module}/#{@module}" : @module + parent_module = parent ? parent.module : nil + parent_module ? "#{parent_module}/#{@module}" : @module end def path_names diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb new file mode 100644 index 0000000000000..1b0e91cf60949 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb @@ -0,0 +1,230 @@ +require 'action_dispatch/routing/dsl/scope' + +# You may wish to organize groups of controllers under a namespace. +# Most commonly, you might group a number of administrative controllers +# under an +admin+ namespace. You would place these controllers under +# the app/controllers/admin directory, and you can group them +# together in your router: +# +# namespace "admin" do +# resources :posts, :comments +# end +# +# This will create a number of routes for each of the posts and comments +# controller. For Admin::PostsController, Rails will create: +# +# GET /admin/posts +# GET /admin/posts/new +# POST /admin/posts +# GET /admin/posts/1 +# GET /admin/posts/1/edit +# PATCH/PUT /admin/posts/1 +# DELETE /admin/posts/1 +# +# If you want to route /posts (without the prefix /admin) to +# Admin::PostsController, you could use +# +# scope module: "admin" do +# resources :posts +# end +# +# or, for a single case +# +# resources :posts, module: "admin" +# +# If you want to route /admin/posts to +PostsController+ +# (without the Admin:: module prefix), you could use +# +# scope "/admin" do +# resources :posts +# end +# +# or, for a single case +# +# resources :posts, path: "/admin/posts" +# +# In each of these cases, the named routes remain the same as if you did +# not use scope. In the last case, the following paths map to +# +PostsController+: +# +# GET /admin/posts +# GET /admin/posts/new +# POST /admin/posts +# GET /admin/posts/1 +# GET /admin/posts/1/edit +# PATCH/PUT /admin/posts/1 +# DELETE /admin/posts/1 + +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Scopes a set of routes to the given default options. + # + # Take the following route definition as an example: + # + # scope path: ":account_id", as: "account" do + # resources :projects + # end + # + # This generates helpers such as +account_projects_path+, just like +resources+ does. + # The difference here being that the routes generated are like /:account_id/projects, + # rather than /accounts/:account_id/projects. + # + # === Options + # + # Takes same options as Base#match and Resources#resources. + # + # # route /posts (without the prefix /admin) to Admin::PostsController + # scope module: "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope path: "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+ + # scope as: "sekret" do + # resources :posts + # end + def scope(*args) + Scope.new(self, *args).instance_exec { yield } + self + end + + # Scopes routes to a specific controller + # + # controller "food" do + # match "bacon", action: "bacon" + # end + def controller(controller, options={}) + options[:controller] = controller + scope(options) { yield } + end + + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # + # === Options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ + # options all default to the name of the namespace. + # + # For options, see Base#match. For +:shallow_path+ option, see + # Resources#resources. + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, path: "sekret" do + # resources :posts + # end + # + # # maps to Sekret::PostsController rather than Admin::PostsController + # namespace :admin, module: "sekret" do + # resources :posts + # end + # + # # generates +sekret_posts_path+ rather than +admin_posts_path+ + # namespace :admin, as: "sekret" do + # resources :posts + # end + def namespace(path, options = {}) + path = path.to_s + + defaults = { + module: path, + path: options.fetch(:path, path), + as: options.fetch(:as, path), + shallow_path: options.fetch(:path, path), + shallow_prefix: options.fetch(:as, path) + } + + scope(defaults.merge!(options)) { yield } + end + + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(id: /\d+\.\d+/) do + # resources :posts + # end + # + # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. + # The +id+ parameter must match the constraint passed in for this example. + # + # You may use this to also restrict other parameters: + # + # resources :posts do + # constraints(post_id: /\d+\.\d+/) do + # resources :comments + # end + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(ip: /192\.168\.\d+\.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches?(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end + def constraints(constraints = {}) + scope(:constraints => constraints) { yield } + end + + # Allows you to set default parameters for a route, such as this: + # defaults id: 'home' do + # match 'scoped_pages/(:id)', to: 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. + def defaults(defaults = {}) + scope(:defaults => defaults) { yield } + end + end + + end + end +end From 89d73c129f78b25daaccf6277f23bf0bb7ada182 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 05:23:20 +0530 Subject: [PATCH 06/23] Instance exec on scoping methods to correctly set self Change path normalization methods to class methods Use set accessor to access the set Return an empty hash for hash based scope properties if they are not supplied --- .../routing/dsl/abstract_scope.rb | 4 ++-- .../routing/dsl/abstract_scope/mapping.rb | 7 +++++-- .../routing/dsl/abstract_scope/match.rb | 2 +- .../routing/dsl/abstract_scope/mount.rb | 2 +- .../dsl/abstract_scope/normalization.rb | 5 ++--- .../routing/dsl/abstract_scope/scoping.rb | 20 +++++++++---------- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 35ef509165662..95950d2d05159 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -49,7 +49,7 @@ def initialize(parent, *args) elsif option == :options value = options else - value = options.delete(option) + value = options.delete(option) { |option| {} if %w(defaults path_names constraints).include?(option.to_s) } end # Set instance variables @@ -115,7 +115,7 @@ def options protected def merge_with_slash(parent, child) - normalize_path("#{parent}/#{child}") + self.class.normalize_path("#{parent}/#{child}") end def merge_with_underscore(parent, child) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb index 43893641b1bbe..7d60279d786b2 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb @@ -1,3 +1,6 @@ +require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/dsl/scope' + module ActionDispatch module Routing class Constraints < Endpoint #:nodoc: @@ -71,7 +74,7 @@ def initialize(scope, path, defaults, options) @to = options.delete :to @default_controller = options.delete(:controller) || scope.controller @default_action = options.delete(:action) || scope.action - @as = options.delete :as + @as = options.delete(:as) || scope.as @anchor = options.delete :anchor formatted = options.delete :format @@ -117,7 +120,7 @@ def to_route private def normalize_path!(path, format) - path = Mapper.normalize_path(path) + path = DSL::Scope.normalize_path(path) if format == true "#{path}.:format" diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb index b45f669e0a1b7..8a58e7de60ed5 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb @@ -250,7 +250,7 @@ def path_for_action(action, path) #:nodoc: def action_path(name, path = nil) #:nodoc: name = name.to_sym if name.is_a?(String) - path || @scope[:path_names][name] || name.to_s + path || path_names[name] || name.to_s end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb index 5c6f925618dc6..ec1e3df2c205b 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb @@ -88,7 +88,7 @@ def prefix_name_for_action(as, action) #:nodoc: def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) - prefix = normalize_name(prefix) if prefix + prefix = self.class.normalize_name(prefix) if prefix name_prefix = self.as # if parent_resource diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb index d780751720ff6..9fe71571cf809 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb @@ -4,17 +4,16 @@ module ActionDispatch module Routing module DSL class AbstractScope - protected # Invokes Journey::Router::Utils.normalize_path and ensure that # (:locale) becomes (/:locale) instead of /(:locale). Except # for root cases, where the latter is the correct one. - def normalize_path(path) + def self.normalize_path(path) path = Journey::Router::Utils.normalize_path(path) path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} path end - def normalize_name(name) + def self.normalize_name(name) normalize_path(name)[1..-1].tr("/", "_") end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb index 1b0e91cf60949..a60ff42b7d0ca 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb @@ -89,8 +89,8 @@ class AbstractScope # scope as: "sekret" do # resources :posts # end - def scope(*args) - Scope.new(self, *args).instance_exec { yield } + def scope(*args, &block) + Scope.new(self, *args).instance_exec(&block) self end @@ -99,9 +99,9 @@ def scope(*args) # controller "food" do # match "bacon", action: "bacon" # end - def controller(controller, options={}) + def controller(controller, options={}, &block) options[:controller] = controller - scope(options) { yield } + scope(options, &block) end # Scopes routes to a specific namespace. For example: @@ -142,7 +142,7 @@ def controller(controller, options={}) # namespace :admin, as: "sekret" do # resources :posts # end - def namespace(path, options = {}) + def namespace(path, options = {}, &block) path = path.to_s defaults = { @@ -153,7 +153,7 @@ def namespace(path, options = {}) shallow_prefix: options.fetch(:as, path) } - scope(defaults.merge!(options)) { yield } + scope(defaults.merge!(options), &block) end # === Parameter Restriction @@ -211,8 +211,8 @@ def namespace(path, options = {}) # constraints(Iphone) do # resources :iphones # end - def constraints(constraints = {}) - scope(:constraints => constraints) { yield } + def constraints(constraints = {}, &block) + scope(:constraints => constraints, &block) end # Allows you to set default parameters for a route, such as this: @@ -220,8 +220,8 @@ def constraints(constraints = {}) # match 'scoped_pages/(:id)', to: 'pages#show' # end # Using this, the +:id+ parameter here will default to 'home'. - def defaults(defaults = {}) - scope(:defaults => defaults) { yield } + def defaults(defaults = {}, &block) + scope(:defaults => defaults, &block) end end From 069bf5718c1b03d27d4ed504d6fe551f82bafe30 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 05:52:33 +0530 Subject: [PATCH 07/23] Added concerns --- .../routing/dsl/abstract_scope.rb | 7 +- .../routing/dsl/abstract_scope/concerns.rb | 115 ++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 95950d2d05159..7e1463a4f251f 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -3,6 +3,7 @@ require 'action_dispatch/routing/dsl/abstract_scope/match' require 'action_dispatch/routing/dsl/abstract_scope/http_helpers' require 'action_dispatch/routing/dsl/abstract_scope/scoping' +require 'action_dispatch/routing/dsl/abstract_scope/concerns' module ActionDispatch module Routing @@ -17,12 +18,12 @@ class AbstractScope # Accessors # ========= - attr_accessor :parent, :set, :concerns - attr_reader :controller, :action + attr_accessor :set + attr_reader :controller, :action, :parent def initialize(parent, *args) if parent - @parent, @set, @concerns = parent, parent.set, parent.concerns + @parent, @set, @concerns = parent, parent.set, parent.declared_concerns else @parent, @concerns = nil, {} end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb new file mode 100644 index 0000000000000..661d2905048a1 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb @@ -0,0 +1,115 @@ +# Routing Concerns allow you to declare common routes that can be reused +# inside others resources and routes. +# +# concern :commentable do +# resources :comments +# end +# +# concern :image_attachable do +# resources :images, only: :index +# end +# +# These concerns are used in Resources routing: +# +# resources :messages, concerns: [:commentable, :image_attachable] +# +# or in a scope or namespace: +# +# namespace :posts do +# concerns :commentable +# end +module ActionDispatch + module Routing + module DSL + class AbstractScope + # Define a routing concern using a name. + # + # Concerns may be defined inline, using a block, or handled by + # another object, by passing that object as the second parameter. + # + # The concern object, if supplied, should respond to call, + # which will receive two parameters: + # + # * The current mapper + # * A hash of options which the concern object may use + # + # Options may also be used by concerns defined in a block by accepting + # a block parameter. So, using a block, you might do something as + # simple as limit the actions available on certain resources, passing + # standard resource options through the concern: + # + # concern :commentable do |options| + # resources :comments, options + # end + # + # resources :posts, concerns: :commentable + # resources :archived_posts do + # # Don't allow comments on archived posts + # concerns :commentable, only: [:index, :show] + # end + # + # Or, using a callable object, you might implement something more + # specific to your application, which would be out of place in your + # routes file. + # + # # purchasable.rb + # class Purchasable + # def initialize(defaults = {}) + # @defaults = defaults + # end + # + # def call(mapper, options = {}) + # options = @defaults.merge(options) + # mapper.resources :purchases + # mapper.resources :receipts + # mapper.resources :returns if options[:returnable] + # end + # end + # + # # routes.rb + # concern :purchasable, Purchasable.new(returnable: true) + # + # resources :toys, concerns: :purchasable + # resources :electronics, concerns: :purchasable + # resources :pets do + # concerns :purchasable, returnable: false + # end + # + # Any routing helpers can be used inside a concern. If using a + # callable, they're accessible from the Mapper that's passed to + # call. + def concern(name, callable = nil, &block) + callable ||= lambda { |scope, options| scope.instance_exec(options, &block) } + @concerns[name] = callable + end + + # Use the named concerns + # + # resources :posts do + # concerns :commentable + # end + # + # concerns also work in any routes helper that you want to use: + # + # namespace :posts do + # concerns :commentable + # end + def concerns(*args) + options = args.extract_options! + args.flatten.each do |name| + if concern = @concerns[name] + concern.call(self, options) + else + raise ArgumentError, "No concern named #{name} was found!" + end + end + end + + # All declared concerns in this call to draw + def declared_concerns + @concerns + end + end + end + end +end From de296ae22df10abaa3a80f2a7eed30147ffa731c Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 06:32:37 +0530 Subject: [PATCH 08/23] Implemented controller and concerns method to return the instance variables if no argument is provided --- .../lib/action_dispatch/routing/dsl/abstract_scope.rb | 4 ++-- .../action_dispatch/routing/dsl/abstract_scope/concerns.rb | 6 +----- .../action_dispatch/routing/dsl/abstract_scope/scoping.rb | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 7e1463a4f251f..93a404a7d9b4e 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -19,11 +19,11 @@ class AbstractScope # Accessors # ========= attr_accessor :set - attr_reader :controller, :action, :parent + attr_reader :action, :parent def initialize(parent, *args) if parent - @parent, @set, @concerns = parent, parent.set, parent.declared_concerns + @parent, @set, @concerns = parent, parent.set, parent.concerns else @parent, @concerns = nil, {} end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb index 661d2905048a1..a88e2057b9902 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb @@ -95,6 +95,7 @@ def concern(name, callable = nil, &block) # concerns :commentable # end def concerns(*args) + return @concerns if args.empty? options = args.extract_options! args.flatten.each do |name| if concern = @concerns[name] @@ -104,11 +105,6 @@ def concerns(*args) end end end - - # All declared concerns in this call to draw - def declared_concerns - @concerns - end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb index a60ff42b7d0ca..1b15a5f1e3ddb 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb @@ -99,7 +99,8 @@ def scope(*args, &block) # controller "food" do # match "bacon", action: "bacon" # end - def controller(controller, options={}, &block) + def controller(controller=nil, options={}, &block) + return @controller unless controller options[:controller] = controller scope(options, &block) end From a984aa8f4970eb550288d3214fb0c32ec86b37a3 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 06:44:12 +0530 Subject: [PATCH 09/23] Implemented methods to return constraints and defaults and also act as DSL --- .../routing/dsl/abstract_scope.rb | 27 ++++++++----------- .../dsl/{abstract_scope => }/normalization.rb | 0 .../lib/action_dispatch/routing/dsl/scope.rb | 5 ++++ .../dsl/{abstract_scope => scope}/concerns.rb | 2 +- .../{abstract_scope => scope}/http_helpers.rb | 2 +- .../dsl/{abstract_scope => scope}/mapping.rb | 0 .../dsl/{abstract_scope => scope}/match.rb | 6 ++--- .../dsl/{abstract_scope => scope}/mount.rb | 2 +- .../dsl/{abstract_scope => scope}/scoping.rb | 22 ++++++++++----- 9 files changed, 37 insertions(+), 29 deletions(-) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => }/normalization.rb (100%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/concerns.rb (99%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/http_helpers.rb (97%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/mapping.rb (100%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/match.rb (98%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/mount.rb (99%) rename actionpack/lib/action_dispatch/routing/dsl/{abstract_scope => scope}/scoping.rb (94%) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 93a404a7d9b4e..07dbfe9f98a53 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -1,9 +1,4 @@ -require 'action_dispatch/routing/dsl/abstract_scope/normalization' -require 'action_dispatch/routing/dsl/abstract_scope/mount' -require 'action_dispatch/routing/dsl/abstract_scope/match' -require 'action_dispatch/routing/dsl/abstract_scope/http_helpers' -require 'action_dispatch/routing/dsl/abstract_scope/scoping' -require 'action_dispatch/routing/dsl/abstract_scope/concerns' +require 'action_dispatch/routing/dsl/normalization' module ActionDispatch module Routing @@ -88,11 +83,6 @@ def path_names merge_hashes(parent_path_names, @path_names) end - def constraints - parent_constraints = parent ? parent.constraints : nil - merge_hashes(parent_constraints, @constraints) - end - def shallow? @shallow end @@ -104,11 +94,6 @@ def blocks merged end - def defaults - parent_defaults = parent ? parent.defaults : nil - merge_hashes(parent_defaults, @defaults) - end - def options parent_options = parent ? parent.options : nil merge_hashes(parent_options, @options) @@ -130,6 +115,16 @@ def merge_hashes(parent, child) def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end + + def _defaults + parent_defaults = parent ? parent.defaults : nil + merge_hashes(parent_defaults, @defaults) + end + + def _constraints + parent_constraints = parent ? parent.constraints : nil + merge_hashes(parent_constraints, @constraints) + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/normalization.rb similarity index 100% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/normalization.rb rename to actionpack/lib/action_dispatch/routing/dsl/normalization.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index b1e28de1570b5..50734063c24a0 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -1,4 +1,9 @@ require 'action_dispatch/routing/dsl/abstract_scope' +require 'action_dispatch/routing/dsl/scope/mount' +require 'action_dispatch/routing/dsl/scope/match' +require 'action_dispatch/routing/dsl/scope/http_helpers' +require 'action_dispatch/routing/dsl/scope/scoping' +require 'action_dispatch/routing/dsl/scope/concerns' module ActionDispatch module Routing diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb similarity index 99% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb index a88e2057b9902..b714dce815ee1 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/concerns.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb @@ -21,7 +21,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + class Scope < AbstractScope # Define a routing concern using a name. # # Concerns may be defined inline, using a block, or handled by diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb similarity index 97% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb index a79e9803e74bf..8a8b18e1c55e7 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/http_helpers.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + class Scope < AbstractScope # Define a route that only recognizes HTTP GET. # For supported arguments, see match[rdoc-ref:Base#match] # diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb similarity index 100% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mapping.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb similarity index 98% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index 8a58e7de60ed5..adf2e6cffad5e 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -1,9 +1,9 @@ -require 'action_dispatch/routing/dsl/abstract_scope/mapping' +require 'action_dispatch/routing/dsl/scope/mapping' module ActionDispatch module Routing module DSL - class AbstractScope + class Scope < AbstractScope # Matches a url pattern to one or more routes. # # You should not use the `match` method in your router @@ -241,7 +241,7 @@ def add_route(action, options) # :nodoc: mapping = Mapping.build(self, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) + set.add_route(app, conditions, requirements, defaults, as, anchor) end def path_for_action(action, path) #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb similarity index 99% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb index ec1e3df2c205b..6aea5e2589f71 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/mount.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + class Scope < AbstractScope # Mount a Rack-based application to be used within the application. # # mount SomeRackApp, at: "some_route" diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb similarity index 94% rename from actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb rename to actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb index 1b15a5f1e3ddb..ec30965596d43 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope/scoping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb @@ -1,5 +1,3 @@ -require 'action_dispatch/routing/dsl/scope' - # You may wish to organize groups of controllers under a namespace. # Most commonly, you might group a number of administrative controllers # under an +admin+ namespace. You would place these controllers under @@ -58,7 +56,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + class Scope < AbstractScope # Scopes a set of routes to the given default options. # # Take the following route definition as an example: @@ -212,8 +210,13 @@ def namespace(path, options = {}, &block) # constraints(Iphone) do # resources :iphones # end - def constraints(constraints = {}, &block) - scope(:constraints => constraints, &block) + def constraints(constraints = nil, &block) + if constraints.nil? + _constraints + else + constraints ||= {} + scope(:constraints => constraints, &block) + end end # Allows you to set default parameters for a route, such as this: @@ -221,8 +224,13 @@ def constraints(constraints = {}, &block) # match 'scoped_pages/(:id)', to: 'pages#show' # end # Using this, the +:id+ parameter here will default to 'home'. - def defaults(defaults = {}, &block) - scope(:defaults => defaults, &block) + def defaults(defaults = nil, &block) + if defaults.nil? + _defaults + else + defaults ||= {} + scope(:defaults => defaults, &block) + end end end From bb2e8e343c4722a709bfc159e2b8b27f50abcc2e Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 4 Jul 2014 08:01:25 +0530 Subject: [PATCH 10/23] Don't merge permanently with parent hash --- actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 07dbfe9f98a53..228ab4bc3675a 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -109,7 +109,7 @@ def merge_with_underscore(parent, child) end def merge_hashes(parent, child) - (parent || {}).except(*override_keys(child)).merge!(child) + (parent || {}).except(*override_keys(child)).merge(child) end def override_keys(child) #:nodoc: From 4bb8b4b0db7c80cc8e110ca55b813c630a40c5a9 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Sun, 13 Jul 2014 12:42:15 +0530 Subject: [PATCH 11/23] Cleaned out match to incorporate MatchRoute MatchRoute is yet to be implemented --- .../routing/dsl/scope/match.rb | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index adf2e6cffad5e..6f4ae5382e62a 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -159,54 +159,38 @@ class Scope < AbstractScope # [:format] # Allows you to specify the default value for optional +format+ # segment or disable it by supplying +false+. - def match(path, *rest) - if rest.empty? && Hash === path - options = path - path, to = options.find { |name, _value| name.is_a?(String) } - - case to - when Symbol - options[:action] = to - when String - if to =~ /#/ - options[:to] = to - else - options[:controller] = to - end - else - options[:to] = to - end - - options.delete(path) - paths = [path] - else - options = rest.pop || {} - paths = [path] + rest + def match(*args) + *paths, options = args # Zero or more paths and a hash of options + if paths.empty? + # If we used the match shorthand, set the path and options[:to] + paths[0], to = options.find { |name, _value| name.is_a?(String) } + options.delete(paths[0]) # Delete the path from the options hash end - options[:anchor] = true unless options.key?(:anchor) - - if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) - raise ArgumentError, "Unknown scope #{on.inspect} given to :on" - end + # Process to and merge into options + process_option_to!(to, options) - if controller && action - options[:to] ||= "#{controller}##{action}" - end + # Now iterate over each path and instantiate a MatchRoute object + # Instantiation of such an object also generates the route on the + # routing table + paths.each { |path| MatchRoute.new(self, path, options) } - paths.each do |_path| - route_options = options.dup - route_options[:path] ||= _path if _path.is_a?(String) + self + end - path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, route_options) - route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - route_options[:to].tr!("-", "_") + def process_option_to!to, (options) + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to end - - decomposed_match(_path, route_options) + else + options[:to] = to end - self end def using_match_shorthand?(path, options) From b577eca45ba762b7ddc28b98c01f1ef4fb5fb032 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Mon, 14 Jul 2014 04:20:36 +0530 Subject: [PATCH 12/23] Add match_route class to handle creating routes on match statements --- .../routing/dsl/abstract_scope.rb | 6 +-- .../routing/dsl/match_route.rb | 46 +++++++++++++++++++ .../routing/dsl/scope/concerns.rb | 2 +- .../routing/dsl/scope/match.rb | 1 + .../routing/dsl/scope/scoping.rb | 20 +++----- 5 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/match_route.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 228ab4bc3675a..48826eab07644 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -14,7 +14,7 @@ class AbstractScope # Accessors # ========= attr_accessor :set - attr_reader :action, :parent + attr_reader :controller, :action, :concerns, :parent def initialize(parent, *args) if parent @@ -116,12 +116,12 @@ def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end - def _defaults + def defaults parent_defaults = parent ? parent.defaults : nil merge_hashes(parent_defaults, @defaults) end - def _constraints + def constraints parent_constraints = parent ? parent.constraints : nil merge_hashes(parent_constraints, @constraints) end diff --git a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb new file mode 100644 index 0000000000000..ee6c41a3cef3d --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb @@ -0,0 +1,46 @@ +# MatchRoute is a special scope in the sense that it has some additonal options +# like :anchor, :to, :via and :format that are specific to routes +require 'action_dispatch/routing/dsl/scope' + +module ActionDispatch + module Routing + module DSL + class MatchRoute < Scope + ROUTE_OPTIONS = [:anchor, :format, :to, :via] + + def initialize(*args) + options_path = (args.extract_options || {})[:path] + super # Let AbstractScope handle all stuff like setting ivar + # Now handle route options + + # Set anchor to true by default unless a value is supplied + @anchor = true unless options.key?(:anchor) + + # Assign options[:to] to @to and then proceed to set an approriate value if + # it evaluates to false + unless @to = options[:to] + # If we have a controller and action then set 'to' to 'controller#action' + # if it is nil + if controller && action + @to = "#{controller}##{action}" + else + # If @to is still nill then convert the path by assuming the entire + # path to represent the controller and the last segment as action e.g. + # match '/admin/controller/action' + # is tanslated as match controller: 'admin/controller', action: 'action' + # + # But before we do that we need to check if the user specified an optional + # format like (.:format) + # If so then we need to consider the path without the optional parameter + *controllers, action = @path.split('/') + action = action.to_s.sub(/\(\.:format\)$/, '') + @to = "#{controllers.select(&:present?).join('/')}##{action}" + end + end + # Change all '-' to '_' in the to + @to.tr!('-', '_') + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb index b714dce815ee1..87c15596e9b4f 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb @@ -95,7 +95,7 @@ def concern(name, callable = nil, &block) # concerns :commentable # end def concerns(*args) - return @concerns if args.empty? + return super() if args.empty? options = args.extract_options! args.flatten.each do |name| if concern = @concerns[name] diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index 6f4ae5382e62a..a1e269fd312ce 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -1,4 +1,5 @@ require 'action_dispatch/routing/dsl/scope/mapping' +require 'action_dispatch/routing/dsl/match_route' module ActionDispatch module Routing diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb index ec30965596d43..b4fb6b3fcaf6a 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb @@ -98,7 +98,7 @@ def scope(*args, &block) # match "bacon", action: "bacon" # end def controller(controller=nil, options={}, &block) - return @controller unless controller + return super() unless controller options[:controller] = controller scope(options, &block) end @@ -211,12 +211,9 @@ def namespace(path, options = {}, &block) # resources :iphones # end def constraints(constraints = nil, &block) - if constraints.nil? - _constraints - else - constraints ||= {} - scope(:constraints => constraints, &block) - end + return super() if constraints.nil? + constraints ||= {} + scope(:constraints => constraints, &block) end # Allows you to set default parameters for a route, such as this: @@ -225,12 +222,9 @@ def constraints(constraints = nil, &block) # end # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = nil, &block) - if defaults.nil? - _defaults - else - defaults ||= {} - scope(:defaults => defaults, &block) - end + return super() if defaults.nil? + defaults ||= {} + scope(:defaults => defaults, &block) end end From 829c52e0dcd275737c39beb1d1ed35dc111e74fe Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Sat, 2 Aug 2014 22:58:20 +0530 Subject: [PATCH 13/23] Set @path variable from name if name is in path names --- actionpack/lib/action_dispatch/routing/dsl/match_route.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb index ee6c41a3cef3d..37edfa404be2a 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb @@ -9,7 +9,7 @@ class MatchRoute < Scope ROUTE_OPTIONS = [:anchor, :format, :to, :via] def initialize(*args) - options_path = (args.extract_options || {})[:path] + # options_path = args.extract_options[:path] || nil super # Let AbstractScope handle all stuff like setting ivar # Now handle route options @@ -39,6 +39,9 @@ def initialize(*args) end # Change all '-' to '_' in the to @to.tr!('-', '_') + + # Set @path from path names if available + @path = name if name = path_names[@path.to_sym] end end end From 05c691e62651d7171d2b50787aea379289615f83 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Wed, 13 Aug 2014 09:33:11 +0530 Subject: [PATCH 14/23] Changed AbstractScope to a module Because modules are a better way to share code instead of treating concrete classes as abstract when no such construct is supported by the language itself --- .../action_dispatch/routing/dsl/abstract_scope.rb | 4 ++-- .../lib/action_dispatch/routing/dsl/match_route.rb | 7 ++++--- .../action_dispatch/routing/dsl/normalization.rb | 2 +- .../lib/action_dispatch/routing/dsl/scope.rb | 14 ++++++++------ .../action_dispatch/routing/dsl/scope/concerns.rb | 2 +- .../routing/dsl/scope/http_helpers.rb | 2 +- .../lib/action_dispatch/routing/dsl/scope/match.rb | 2 +- .../lib/action_dispatch/routing/dsl/scope/mount.rb | 2 +- .../action_dispatch/routing/dsl/scope/scoping.rb | 2 +- 9 files changed, 20 insertions(+), 17 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 48826eab07644..1471c2dd5d9be 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -3,7 +3,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + module AbstractScope # Constants # ========= URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] @@ -45,7 +45,7 @@ def initialize(parent, *args) elsif option == :options value = options else - value = options.delete(option) { |option| {} if %w(defaults path_names constraints).include?(option.to_s) } + value = options.delete(option) { |_option| {} if %w(defaults path_names constraints).include?(_option.to_s) } end # Set instance variables diff --git a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb index 37edfa404be2a..9f193078fee24 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb @@ -1,11 +1,12 @@ # MatchRoute is a special scope in the sense that it has some additonal options # like :anchor, :to, :via and :format that are specific to routes -require 'action_dispatch/routing/dsl/scope' +require 'action_dispatch/routing/dsl/abstract_scope' module ActionDispatch module Routing module DSL - class MatchRoute < Scope + class MatchRoute + include AbstractScope ROUTE_OPTIONS = [:anchor, :format, :to, :via] def initialize(*args) @@ -38,7 +39,7 @@ def initialize(*args) end end # Change all '-' to '_' in the to - @to.tr!('-', '_') + @to.tr!('-', '_') if @to.is_a? String # Set @path from path names if available @path = name if name = path_names[@path.to_sym] diff --git a/actionpack/lib/action_dispatch/routing/dsl/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/normalization.rb index 9fe71571cf809..5a6666ee42da7 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/normalization.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/normalization.rb @@ -3,7 +3,7 @@ module ActionDispatch module Routing module DSL - class AbstractScope + module AbstractScope # Invokes Journey::Router::Utils.normalize_path and ensure that # (:locale) becomes (/:locale) instead of /(:locale). Except # for root cases, where the latter is the correct one. diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index 50734063c24a0..03ef866e19ded 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -1,15 +1,17 @@ require 'action_dispatch/routing/dsl/abstract_scope' -require 'action_dispatch/routing/dsl/scope/mount' -require 'action_dispatch/routing/dsl/scope/match' -require 'action_dispatch/routing/dsl/scope/http_helpers' -require 'action_dispatch/routing/dsl/scope/scoping' -require 'action_dispatch/routing/dsl/scope/concerns' module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope + include AbstractScope end end end end + +require 'action_dispatch/routing/dsl/scope/mount' +require 'action_dispatch/routing/dsl/scope/match' +require 'action_dispatch/routing/dsl/scope/http_helpers' +require 'action_dispatch/routing/dsl/scope/scoping' +require 'action_dispatch/routing/dsl/scope/concerns' diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb index 87c15596e9b4f..8c4c7c9a81b90 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/concerns.rb @@ -21,7 +21,7 @@ module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope # Define a routing concern using a name. # # Concerns may be defined inline, using a block, or handled by diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb index 8a8b18e1c55e7..ee3a096b2c4e3 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/http_helpers.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope # Define a route that only recognizes HTTP GET. # For supported arguments, see match[rdoc-ref:Base#match] # diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index a1e269fd312ce..e30278539c98b 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -4,7 +4,7 @@ module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope # Matches a url pattern to one or more routes. # # You should not use the `match` method in your router diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb index 6aea5e2589f71..e0d4e1dd25f00 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope # Mount a Rack-based application to be used within the application. # # mount SomeRackApp, at: "some_route" diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb index b4fb6b3fcaf6a..6cfcd7cd58070 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/scoping.rb @@ -56,7 +56,7 @@ module ActionDispatch module Routing module DSL - class Scope < AbstractScope + class Scope # Scopes a set of routes to the given default options. # # Take the following route definition as an example: From 02053c474056cadea09ff3de3263d783d93c3b14 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Wed, 13 Aug 2014 15:32:02 +0530 Subject: [PATCH 15/23] Fixed prefix_name_for_action A bug in prefix_name_for_action prevented from proper route helper generation Added default_url_options --- .../routing/dsl/abstract_scope.rb | 2 +- .../routing/dsl/normalization.rb | 25 +++++++++++-------- .../lib/action_dispatch/routing/dsl/scope.rb | 13 ++++++++++ .../routing/dsl/scope/mapping.rb | 3 +-- .../routing/dsl/scope/match.rb | 7 ++++-- .../routing/dsl/scope/mount.rb | 6 +---- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 1471c2dd5d9be..76233bd6d1ea5 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -49,7 +49,7 @@ def initialize(parent, *args) end # Set instance variables - instance_variable_set(:"@#{option}", value) if value + instance_variable_set(:"@#{option}", value || nil) end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/normalization.rb b/actionpack/lib/action_dispatch/routing/dsl/normalization.rb index 5a6666ee42da7..d2c60558420e9 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/normalization.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/normalization.rb @@ -1,20 +1,25 @@ require 'action_dispatch/journey' +require 'active_support/concern' module ActionDispatch module Routing module DSL module AbstractScope - # Invokes Journey::Router::Utils.normalize_path and ensure that - # (:locale) becomes (/:locale) instead of /(:locale). Except - # for root cases, where the latter is the correct one. - def self.normalize_path(path) - path = Journey::Router::Utils.normalize_path(path) - path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} - path - end + extend ActiveSupport::Concern + + included do |base| + # Invokes Journey::Router::Utils.normalize_path and ensure that + # (:locale) becomes (/:locale) instead of /(:locale). Except + # for root cases, where the latter is the correct one. + def base.normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} + path + end - def self.normalize_name(name) - normalize_path(name)[1..-1].tr("/", "_") + def base.normalize_name(name) + normalize_path(name)[1..-1].tr("/", "_") + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index 03ef866e19ded..94499e9ee6e95 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -5,6 +5,19 @@ module Routing module DSL class Scope include AbstractScope + + def method_missing(method, *args) + @count ||= 0 + @count += 1 + msg = "#{@count}) Missing :#{method}" + divider = "="*msg.length + puts divider, msg, divider + end + + def default_url_options=(options) + @set.default_url_options = options + end + alias_method :default_url_options, :default_url_options= end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb index 7d60279d786b2..9c7a9b37616ad 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb @@ -1,5 +1,4 @@ require 'action_dispatch/routing/redirection' -require 'action_dispatch/routing/dsl/scope' module ActionDispatch module Routing @@ -98,7 +97,7 @@ def initialize(scope, path, defaults, options) if options_constraints.is_a?(Hash) split_constraints path_params, options_constraints options_constraints.each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + if DSL::Scope::URL_OPTIONS.include?(key) && (String === default || Fixnum === default) @defaults[key] ||= default end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index e30278539c98b..96e0f495b6de7 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -1,5 +1,5 @@ require 'action_dispatch/routing/dsl/scope/mapping' -require 'action_dispatch/routing/dsl/match_route' +# require 'action_dispatch/routing/dsl/match_route' module ActionDispatch module Routing @@ -174,7 +174,10 @@ def match(*args) # Now iterate over each path and instantiate a MatchRoute object # Instantiation of such an object also generates the route on the # routing table - paths.each { |path| MatchRoute.new(self, path, options) } + paths.each do |path| + decomposed_match path, options + #MatchRoute.new(self, path, options) + end self end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb index e0d4e1dd25f00..f250b4ed13d06 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb @@ -78,11 +78,7 @@ def mounted?; true; end end def prefix_name_for_action(as, action) #:nodoc: - if as - prefix = as - # elsif !canonical_action?(action, @scope[:scope_level]) - # prefix = action - end + prefix = as || action prefix.to_s.tr('-', '_') if prefix end From 465e21c9155834c73949a7738241b1ff1f8ec78e Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Wed, 13 Aug 2014 16:09:30 +0530 Subject: [PATCH 16/23] Added redirection module to Scope Fixed processing of to option and added root method --- .../lib/action_dispatch/routing/dsl/scope.rb | 2 ++ .../routing/dsl/scope/mapping.rb | 1 - .../routing/dsl/scope/match.rb | 35 ++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index 94499e9ee6e95..ecfdb9c77f58b 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -1,10 +1,12 @@ require 'action_dispatch/routing/dsl/abstract_scope' +require 'action_dispatch/routing/redirection' module ActionDispatch module Routing module DSL class Scope include AbstractScope + include Redirection def method_missing(method, *args) @count ||= 0 diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb index 9c7a9b37616ad..87ccd1452032e 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb @@ -83,7 +83,6 @@ def initialize(scope, path, defaults, options) path = normalize_path! path, formatted ast = path_ast path path_params = path_params ast - options = normalize_options!(options, formatted, path_params, ast, scope.module) diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index 96e0f495b6de7..55d738e3a7180 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -166,10 +166,10 @@ def match(*args) # If we used the match shorthand, set the path and options[:to] paths[0], to = options.find { |name, _value| name.is_a?(String) } options.delete(paths[0]) # Delete the path from the options hash - end - # Process to and merge into options - process_option_to!(to, options) + # Process to and merge into options + process_option_to!(to, options) + end # Now iterate over each path and instantiate a MatchRoute object # Instantiation of such an object also generates the route on the @@ -182,7 +182,34 @@ def match(*args) self end - def process_option_to!to, (options) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of config/routes.rb, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(path, options={}) + if path.is_a?(String) + options[:to] = path + elsif path.is_a?(Hash) and options.empty? + options = path + else + raise ArgumentError, "must be called with a path and/or options" + end + + match '/', { :as => :root, :via => :get }.merge!(options) + end + + protected + + def process_option_to!(to, options) case to when Symbol options[:action] = to From adec38932fe24fab6b7e2d3401c0e584df6ca6da Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Wed, 13 Aug 2014 16:33:17 +0530 Subject: [PATCH 17/23] Set anchor to true by default --- .../action_dispatch/routing/dsl/scope/match.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index 55d738e3a7180..31ec3b86deb6d 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -171,12 +171,13 @@ def match(*args) process_option_to!(to, options) end + options[:anchor] = true unless options.key?(:anchor) + # Now iterate over each path and instantiate a MatchRoute object # Instantiation of such an object also generates the route on the # routing table paths.each do |path| - decomposed_match path, options - #MatchRoute.new(self, path, options) + add_route(path, options) end self @@ -228,14 +229,6 @@ def using_match_shorthand?(path, options) path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$} end - def decomposed_match(path, options) # :nodoc: - if on = options.delete(:on) - send(on) { decomposed_match(path, options) } - else - add_route(path, options) - end - end - def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) raise ArgumentError, "path is required" if path.blank? @@ -253,7 +246,7 @@ def add_route(action, options) # :nodoc: else options[:as] = name_for_action(options[:as], action) end - + mapping = Mapping.build(self, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route set.add_route(app, conditions, requirements, defaults, as, anchor) From 19af8c4dfe89f8e001d27ed879d7b68148691d33 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Wed, 13 Aug 2014 20:55:26 +0530 Subject: [PATCH 18/23] Fixed module method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append '/' only if child module exists along with parent module Added has_named_route? method Added some outline to match_route but we’ll leave that aside for the time being and replace the implementation in future after we get the whole thing working --- .../routing/dsl/abstract_scope.rb | 11 ++++++-- .../routing/dsl/match_route.rb | 26 ++++++++++++++++++- .../lib/action_dispatch/routing/dsl/scope.rb | 5 ++++ .../routing/dsl/scope/mapping.rb | 8 +++--- .../routing/dsl/scope/match.rb | 17 ++++++++++-- .../lib/action_dispatch/routing/route_set.rb | 4 +++ 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb index 76233bd6d1ea5..7d6cf84634add 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/abstract_scope.rb @@ -74,8 +74,15 @@ def shallow_prefix end def module - parent_module = parent ? parent.module : nil - parent_module ? "#{parent_module}/#{@module}" : @module + if parent && parent.module + if @module + "#{parent.module}/#{@module}" + else + parent.module + end + else + @module + end end def path_names diff --git a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb index 9f193078fee24..5d39c02cc8aeb 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/match_route.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/match_route.rb @@ -42,8 +42,32 @@ def initialize(*args) @to.tr!('-', '_') if @to.is_a? String # Set @path from path names if available - @path = name if name = path_names[@path.to_sym] + @path = path_names[@path.to_sym] || @path + + # If we still can't set a path then there is something wrong + raise ArgumentError, "path is required" if @path.blank? + + add_to_router end + + protected + def add_to_router + build + setup + add_route + end + + def build + # + end + + def setup + # + end + + def add_route + # + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index ecfdb9c77f58b..705264e47865e 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -20,6 +20,11 @@ def default_url_options=(options) @set.default_url_options = options end alias_method :default_url_options, :default_url_options= + + # Query if the following named route was already defined. + def has_named_route?(name) + @set.named_routes.routes[name.to_sym] + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb index 87ccd1452032e..586573f52f625 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb @@ -75,6 +75,7 @@ def initialize(scope, path, defaults, options) @default_action = options.delete(:action) || scope.action @as = options.delete(:as) || scope.as @anchor = options.delete :anchor + @set = scope.set formatted = options.delete :format via = Array(options.delete(:via) { [] }) @@ -83,6 +84,7 @@ def initialize(scope, path, defaults, options) path = normalize_path! path, formatted ast = path_ast path path_params = path_params ast + options = normalize_options!(options, formatted, path_params, ast, scope.module) @@ -236,7 +238,7 @@ def app(blocks) if blocks.any? Constraints.new(dispatcher, blocks, true) else - dispatcher + dispatcher(defaults) end end end @@ -333,8 +335,8 @@ def path_ast(path) parser.parse path end - def dispatcher - Routing::RouteSet::Dispatcher.new(defaults) + def dispatcher(defaults) + @set.dispatcher defaults end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index 31ec3b86deb6d..b907d69b964b5 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -173,11 +173,24 @@ def match(*args) options[:anchor] = true unless options.key?(:anchor) + if controller && action + options[:to] ||= "#{controller}##{action}" + end + # Now iterate over each path and instantiate a MatchRoute object # Instantiation of such an object also generates the route on the # routing table paths.each do |path| - add_route(path, options) + route_options = options.dup + route_options[:path] ||= path if path.is_a?(String) + + path_without_format = path.to_s.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format, route_options) + route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") + end + + add_route(path, route_options) end self @@ -246,7 +259,7 @@ def add_route(action, options) # :nodoc: else options[:as] = name_for_action(options[:as], action) end - + mapping = Mapping.build(self, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route set.add_route(app, conditions, requirements, defaults, as, anchor) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 468c0403af26a..d9e270c1866f2 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -333,6 +333,10 @@ def clear! @prepend.each { |blk| eval_block(blk) } end + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + module MountedHelpers #:nodoc: extend ActiveSupport::Concern include UrlFor From af4d7a3b65614de1ebdea6e2455ce64f4bc033e6 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 15 Aug 2014 00:25:26 +0530 Subject: [PATCH 19/23] Duplicate dispatch/routing_test.rb for our own tests --- actionpack/test/dispatch/new_routing_test.rb | 4365 ++++++++++++++++++ 1 file changed, 4365 insertions(+) create mode 100644 actionpack/test/dispatch/new_routing_test.rb diff --git a/actionpack/test/dispatch/new_routing_test.rb b/actionpack/test/dispatch/new_routing_test.rb new file mode 100644 index 0000000000000..778dbfc74d2ae --- /dev/null +++ b/actionpack/test/dispatch/new_routing_test.rb @@ -0,0 +1,4365 @@ +# encoding: UTF-8 +require 'erb' +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 YoutubeFavoritesRedirector + def self.call(params, request) + "http://www.youtube.com/watch?v=#{params[:youtube_id]}" + end + end + + def test_logout + draw do + controller :sessions do + delete 'logout' => :destroy + end + end + + delete '/logout' + assert_equal 'sessions#destroy', @response.body + + assert_equal '/logout', logout_path + assert_equal '/logout', url_for(:controller => 'sessions', :action => 'destroy', :only_path => true) + end + + def test_login + draw do + default_url_options :host => "rubyonrails.org" + + controller :sessions do + get 'login' => :new + post 'login' => :create + end + end + + get '/login' + assert_equal 'sessions#new', @response.body + assert_equal '/login', login_path + + post '/login' + assert_equal 'sessions#create', @response.body + + assert_equal '/login', url_for(:controller => 'sessions', :action => 'create', :only_path => true) + assert_equal '/login', url_for(:controller => 'sessions', :action => 'new', :only_path => true) + + assert_equal 'http://rubyonrails.org/login', url_for(:controller => 'sessions', :action => 'create') + assert_equal 'http://rubyonrails.org/login', login_url + end + + def test_login_redirect + draw do + get 'account/login', :to => redirect("/login") + end + + get '/account/login' + verify_redirect 'http://www.example.com/login' + end + + def test_logout_redirect_without_to + draw do + get 'account/logout' => redirect("/logout"), :as => :logout_redirect + end + + assert_equal '/account/logout', logout_redirect_path + get '/account/logout' + verify_redirect 'http://www.example.com/logout' + end + + def test_namespace_redirect + draw do + namespace :private do + root :to => redirect('/private/index') + get "index", :to => 'private#index' + end + end + + get '/private' + verify_redirect 'http://www.example.com/private/index' + end + + def test_namespace_with_controller_segment + assert_raise(ArgumentError) do + draw do + namespace :admin do + get '/:controller(/:action(/:id(.:format)))' + end + end + end + end + + def test_namespace_without_controller_segment + draw do + namespace :admin do + get 'hello/:controllers/:action' + end + end + get '/admin/hello/foo/new' + assert_equal 'foo', @request.params["controllers"] + end + + def test_session_singleton_resource + draw do + resource :session do + get :create + post :reset + end + end + + get '/session' + assert_equal 'sessions#create', @response.body + assert_equal '/session', session_path + + post '/session' + assert_equal 'sessions#create', @response.body + + put '/session' + assert_equal 'sessions#update', @response.body + + delete '/session' + assert_equal 'sessions#destroy', @response.body + + get '/session/new' + assert_equal 'sessions#new', @response.body + assert_equal '/session/new', new_session_path + + get '/session/edit' + assert_equal 'sessions#edit', @response.body + assert_equal '/session/edit', edit_session_path + + post '/session/reset' + assert_equal 'sessions#reset', @response.body + assert_equal '/session/reset', reset_session_path + end + + def test_session_info_nested_singleton_resource + draw do + resource :session do + resource :info + end + end + + get '/session/info' + assert_equal 'infos#show', @response.body + assert_equal '/session/info', session_info_path + end + + def test_member_on_resource + draw do + resource :session do + member do + get :crush + end + end + end + + get '/session/crush' + assert_equal 'sessions#crush', @response.body + assert_equal '/session/crush', crush_session_path + end + + def test_redirect_modulo + draw do + get 'account/modulo/:name', :to => redirect("/%{name}s") + end + + get '/account/modulo/name' + verify_redirect 'http://www.example.com/names' + end + + def test_redirect_proc + draw do + get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" } + end + + get '/account/proc/person' + verify_redirect 'http://www.example.com/people' + end + + def test_redirect_proc_with_request + draw do + get 'account/proc_req' => redirect {|params, req| "/#{req.method}" } + end + + get '/account/proc_req' + verify_redirect 'http://www.example.com/GET' + end + + def test_redirect_hash_with_subdomain + draw do + get 'mobile', :to => redirect(:subdomain => 'mobile') + end + + get '/mobile' + verify_redirect 'http://mobile.example.com/mobile' + end + + def test_redirect_hash_with_domain_and_path + draw do + get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + end + + get '/documentation' + verify_redirect 'http://www.example-documentation.com' + end + + def test_redirect_hash_with_path + draw do + get 'new_documentation', :to => redirect(:path => '/documentation/new') + end + + get '/new_documentation' + verify_redirect 'http://www.example.com/documentation/new' + end + + def test_redirect_hash_with_host + draw do + get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + end + + get '/super_new_documentation?section=top' + verify_redirect 'http://super-docs.com/super_new_documentation?section=top' + end + + def test_redirect_hash_path_substitution + draw do + get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + end + + get '/stores/iernest' + verify_redirect 'http://stores.example.com/iernest' + end + + def test_redirect_hash_path_substitution_with_catch_all + draw do + get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + end + + get '/stores/iernest/products' + verify_redirect 'http://stores.example.com/iernest/products' + end + + def test_redirect_class + draw do + get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) + end + + get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' + verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' + end + + def test_openid + draw do + match 'openid/login', :via => [:get, :post], :to => "openid#login" + end + + get '/openid/login' + assert_equal 'openid#login', @response.body + + post '/openid/login' + assert_equal 'openid#login', @response.body + end + + def test_bookmarks + draw do + scope "bookmark", :controller => "bookmarks", :as => :bookmark do + get :new, :path => "build" + post :create, :path => "create", :as => "" + put :update + get :remove, :action => :destroy, :as => :remove + end + end + + get '/bookmark/build' + assert_equal 'bookmarks#new', @response.body + assert_equal '/bookmark/build', bookmark_new_path + + post '/bookmark/create' + assert_equal 'bookmarks#create', @response.body + assert_equal '/bookmark/create', bookmark_path + + put '/bookmark/update' + assert_equal 'bookmarks#update', @response.body + assert_equal '/bookmark/update', bookmark_update_path + + get '/bookmark/remove' + assert_equal 'bookmarks#destroy', @response.body + assert_equal '/bookmark/remove', bookmark_remove_path + end + + def test_pagemarks + draw do + scope "pagemark", :controller => "pagemarks", :as => :pagemark do + get "new", :path => "build" + post "create", :as => "" + put "update" + get "remove", :action => :destroy, :as => :remove + end + end + + get '/pagemark/build' + assert_equal 'pagemarks#new', @response.body + assert_equal '/pagemark/build', pagemark_new_path + + post '/pagemark/create' + assert_equal 'pagemarks#create', @response.body + assert_equal '/pagemark/create', pagemark_path + + put '/pagemark/update' + assert_equal 'pagemarks#update', @response.body + assert_equal '/pagemark/update', pagemark_update_path + + get '/pagemark/remove' + assert_equal 'pagemarks#destroy', @response.body + assert_equal '/pagemark/remove', pagemark_remove_path + end + + def test_admin + draw do + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin' => "queenbee#index" + end + + constraints ::TestRoutingMapper::IpRestrictor do + get 'admin/accounts' => "queenbee#accounts" + end + + get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor + end + + get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#index', @response.body + + get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + + get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#accounts', @response.body + + get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#passwords', @response.body + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + end + + def test_global + draw do + controller(:global) do + get 'global/hide_notice' + get 'global/export', :action => :export, :as => :export_request + get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } + get 'global/:action' + end + end + + 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', export_request_path + assert_equal '/global/hide_notice', global_hide_notice_path + assert_equal '/export/123/foo.txt', export_download_path(:id => 123, :file => 'foo.txt') + end + + def test_local + draw do + get "/local/:action", :controller => "local" + end + + get '/local/dashboard' + assert_equal 'local#dashboard', @response.body + end + + # tests the use of dup in url_for + def test_url_for_with_no_side_effects + draw do + get "/projects/status(.:format)" + end + + # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) + original_options = {:controller => 'projects', :action => 'status'} + options = original_options.dup + + url_for options + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_url_for_does_not_modify_controller + draw do + get "/projects/status(.:format)" + end + + controller = '/projects' + options = {:controller => controller, :action => 'status', :only_path => true} + url = url_for(options) + + assert_equal '/projects/status', url + assert_equal '/projects', controller + end + + # tests the arguments modification free version of define_hash_access + def test_named_route_with_no_side_effects + draw do + resources :customers do + get "profile", :on => :member + end + end + + original_options = { :host => 'test.host' } + options = original_options.dup + + profile_customer_url("customer_model", options) + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_projects_status + draw do + get "/projects/status(.:format)" + end + + assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true) + assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true) + end + + def test_projects + draw do + resources :projects, :controller => :project + end + + get '/projects' + assert_equal 'project#index', @response.body + assert_equal '/projects', projects_path + + post '/projects' + assert_equal 'project#create', @response.body + + get '/projects.xml' + assert_equal 'project#index', @response.body + assert_equal '/projects.xml', projects_path(:format => 'xml') + + get '/projects/new' + assert_equal 'project#new', @response.body + assert_equal '/projects/new', new_project_path + + get '/projects/new.xml' + assert_equal 'project#new', @response.body + assert_equal '/projects/new.xml', new_project_path(:format => 'xml') + + get '/projects/1' + assert_equal 'project#show', @response.body + assert_equal '/projects/1', project_path(:id => '1') + + get '/projects/1.xml' + assert_equal 'project#show', @response.body + assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml') + + get '/projects/1/edit' + assert_equal 'project#edit', @response.body + assert_equal '/projects/1/edit', edit_project_path(:id => '1') + end + + def test_projects_with_post_action_and_new_path_on_collection + draw do + resources :projects, :controller => :project do + post 'new', :action => 'new', :on => :collection, :as => :new + end + end + + post '/projects/new' + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_projects_path + end + + def test_projects_involvements + draw do + resources :projects, :controller => :project do + resources :involvements, :attachments + end + end + + get '/projects/1/involvements' + assert_equal 'involvements#index', @response.body + assert_equal '/projects/1/involvements', project_involvements_path(:project_id => '1') + + get '/projects/1/involvements/new' + assert_equal 'involvements#new', @response.body + assert_equal '/projects/1/involvements/new', new_project_involvement_path(:project_id => '1') + + get '/projects/1/involvements/1' + assert_equal 'involvements#show', @response.body + assert_equal '/projects/1/involvements/1', project_involvement_path(:project_id => '1', :id => '1') + + put '/projects/1/involvements/1' + assert_equal 'involvements#update', @response.body + + delete '/projects/1/involvements/1' + assert_equal 'involvements#destroy', @response.body + + get '/projects/1/involvements/1/edit' + assert_equal 'involvements#edit', @response.body + assert_equal '/projects/1/involvements/1/edit', edit_project_involvement_path(:project_id => '1', :id => '1') + end + + def test_projects_attachments + draw do + resources :projects, :controller => :project do + resources :involvements, :attachments + end + end + + get '/projects/1/attachments' + assert_equal 'attachments#index', @response.body + assert_equal '/projects/1/attachments', project_attachments_path(:project_id => '1') + end + + def test_projects_participants + draw do + resources :projects, :controller => :project do + resources :participants do + put :update_all, :on => :collection + end + end + end + + get '/projects/1/participants' + assert_equal 'participants#index', @response.body + assert_equal '/projects/1/participants', project_participants_path(:project_id => '1') + + put '/projects/1/participants/update_all' + assert_equal 'participants#update_all', @response.body + assert_equal '/projects/1/participants/update_all', update_all_project_participants_path(:project_id => '1') + end + + def test_projects_companies + draw do + resources :projects, :controller => :project do + resources :companies do + resources :people + resource :avatar, :controller => :avatar + end + end + end + + get '/projects/1/companies' + assert_equal 'companies#index', @response.body + assert_equal '/projects/1/companies', project_companies_path(:project_id => '1') + + get '/projects/1/companies/1/people' + assert_equal 'people#index', @response.body + assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1') + + get '/projects/1/companies/1/avatar' + assert_equal 'avatar#show', @response.body + assert_equal '/projects/1/companies/1/avatar', project_company_avatar_path(:project_id => '1', :company_id => '1') + end + + def test_project_manager + draw do + resources :projects do + resource :manager, :as => :super_manager do + post :fire + end + end + end + + get '/projects/1/manager' + assert_equal 'managers#show', @response.body + assert_equal '/projects/1/manager', project_super_manager_path(:project_id => '1') + + get '/projects/1/manager/new' + assert_equal 'managers#new', @response.body + assert_equal '/projects/1/manager/new', new_project_super_manager_path(:project_id => '1') + + post '/projects/1/manager/fire' + assert_equal 'managers#fire', @response.body + assert_equal '/projects/1/manager/fire', fire_project_super_manager_path(:project_id => '1') + end + + def test_project_images + draw do + resources :projects do + resources :images, :as => :funny_images do + post :revise, :on => :member + end + end + end + + get '/projects/1/images' + assert_equal 'images#index', @response.body + assert_equal '/projects/1/images', project_funny_images_path(:project_id => '1') + + get '/projects/1/images/new' + assert_equal 'images#new', @response.body + assert_equal '/projects/1/images/new', new_project_funny_image_path(:project_id => '1') + + post '/projects/1/images/1/revise' + assert_equal 'images#revise', @response.body + assert_equal '/projects/1/images/1/revise', revise_project_funny_image_path(:project_id => '1', :id => '1') + end + + def test_projects_people + draw do + resources :projects do + resources :people do + nested do + scope "/:access_token" do + resource :avatar + end + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end + end + end + end + + get '/projects/1/people' + assert_equal 'people#index', @response.body + assert_equal '/projects/1/people', project_people_path(:project_id => '1') + + get '/projects/1/people/1' + assert_equal 'people#show', @response.body + assert_equal '/projects/1/people/1', project_person_path(:project_id => '1', :id => '1') + + get '/projects/1/people/1/7a2dec8/avatar' + assert_equal 'avatars#show', @response.body + assert_equal '/projects/1/people/1/7a2dec8/avatar', project_person_avatar_path(:project_id => '1', :person_id => '1', :access_token => '7a2dec8') + + put '/projects/1/people/1/accessible_projects' + assert_equal 'people#accessible_projects', @response.body + assert_equal '/projects/1/people/1/accessible_projects', accessible_projects_project_person_path(:project_id => '1', :id => '1') + + post '/projects/1/people/1/resend' + assert_equal 'people#resend', @response.body + assert_equal '/projects/1/people/1/resend', resend_project_person_path(:project_id => '1', :id => '1') + + post '/projects/1/people/1/generate_new_password' + assert_equal 'people#generate_new_password', @response.body + assert_equal '/projects/1/people/1/generate_new_password', generate_new_password_project_person_path(:project_id => '1', :id => '1') + end + + def test_projects_with_resources_path_names + draw do + resources_path_names :correlation_indexes => "info_about_correlation_indexes" + + resources :projects do + get :correlation_indexes, :on => :collection + end + end + + get '/projects/info_about_correlation_indexes' + assert_equal 'projects#correlation_indexes', @response.body + assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path + end + + def test_projects_posts + draw do + resources :projects do + 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 + end + + get '/projects/1/posts' + assert_equal 'posts#index', @response.body + assert_equal '/projects/1/posts', project_posts_path(:project_id => '1') + + get '/projects/1/posts/archive' + assert_equal 'posts#archive', @response.body + assert_equal '/projects/1/posts/archive', archive_project_posts_path(:project_id => '1') + + get '/projects/1/posts/toggle_view' + assert_equal 'posts#toggle_view', @response.body + assert_equal '/projects/1/posts/toggle_view', toggle_view_project_posts_path(:project_id => '1') + + post '/projects/1/posts/1/preview' + assert_equal 'posts#preview', @response.body + assert_equal '/projects/1/posts/1/preview', preview_project_post_path(:project_id => '1', :id => '1') + + get '/projects/1/posts/1/subscription' + assert_equal 'subscriptions#show', @response.body + assert_equal '/projects/1/posts/1/subscription', project_post_subscription_path(:project_id => '1', :post_id => '1') + + get '/projects/1/posts/1/comments' + assert_equal 'comments#index', @response.body + assert_equal '/projects/1/posts/1/comments', project_post_comments_path(:project_id => '1', :post_id => '1') + + post '/projects/1/posts/1/comments/preview' + assert_equal 'comments#preview', @response.body + assert_equal '/projects/1/posts/1/comments/preview', preview_project_post_comments_path(:project_id => '1', :post_id => '1') + end + + def test_replies + draw do + resources :replies do + member do + put :answer, :action => :mark_as_answer + delete :answer, :action => :unmark_as_answer + end + end + end + + put '/replies/1/answer' + assert_equal 'replies#mark_as_answer', @response.body + + delete '/replies/1/answer' + assert_equal 'replies#unmark_as_answer', @response.body + end + + def test_resource_routes_with_only_and_except + draw do + resources :posts, :only => [:index, :show] do + resources :comments, :except => :destroy + end + end + + get '/posts' + assert_equal 'posts#index', @response.body + assert_equal '/posts', posts_path + + get '/posts/1' + assert_equal 'posts#show', @response.body + assert_equal '/posts/1', post_path(:id => 1) + + get '/posts/1/comments' + assert_equal 'comments#index', @response.body + assert_equal '/posts/1/comments', post_comments_path(:post_id => 1) + + post '/posts' + assert_equal 'pass', @response.headers['X-Cascade'] + put '/posts/1' + assert_equal 'pass', @response.headers['X-Cascade'] + delete '/posts/1' + assert_equal 'pass', @response.headers['X-Cascade'] + delete '/posts/1/comments' + assert_equal 'pass', @response.headers['X-Cascade'] + end + + def test_resource_routes_only_create_update_destroy + draw do + resource :past, :only => :destroy + resource :present, :only => :update + resource :future, :only => :create + end + + delete '/past' + assert_equal 'pasts#destroy', @response.body + assert_equal '/past', past_path + + patch '/present' + assert_equal 'presents#update', @response.body + assert_equal '/present', present_path + + put '/present' + assert_equal 'presents#update', @response.body + assert_equal '/present', present_path + + post '/future' + assert_equal 'futures#create', @response.body + assert_equal '/future', future_path + end + + def test_resources_routes_only_create_update_destroy + draw do + resources :relationships, :only => [:create, :destroy] + resources :friendships, :only => [:update] + end + + post '/relationships' + assert_equal 'relationships#create', @response.body + assert_equal '/relationships', relationships_path + + delete '/relationships/1' + assert_equal 'relationships#destroy', @response.body + assert_equal '/relationships/1', relationship_path(1) + + patch '/friendships/1' + assert_equal 'friendships#update', @response.body + assert_equal '/friendships/1', friendship_path(1) + + put '/friendships/1' + assert_equal 'friendships#update', @response.body + assert_equal '/friendships/1', friendship_path(1) + end + + def test_resource_with_slugs_in_ids + draw do + resources :posts + end + + get '/posts/rails-rocks' + assert_equal 'posts#show', @response.body + assert_equal '/posts/rails-rocks', post_path(:id => 'rails-rocks') + end + + def test_resources_for_uncountable_names + draw do + resources :sheep do + get "_it", :on => :member + end + end + + assert_equal '/sheep', sheep_index_path + assert_equal '/sheep/1', sheep_path(1) + assert_equal '/sheep/new', new_sheep_path + assert_equal '/sheep/1/edit', edit_sheep_path(1) + assert_equal '/sheep/1/_it', _it_sheep_path(1) + end + + def test_resource_does_not_modify_passed_options + options = {:id => /.+?/, :format => /json|xml/} + draw { resource :user, options } + assert_equal({:id => /.+?/, :format => /json|xml/}, options) + end + + def test_resources_does_not_modify_passed_options + options = {:id => /.+?/, :format => /json|xml/} + draw { resources :users, options } + assert_equal({:id => /.+?/, :format => /json|xml/}, options) + end + + def test_path_names + draw do + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' + resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do + put :activate, :on => :member + end + end + end + + get '/pt/projetos' + assert_equal 'projects#index', @response.body + assert_equal '/pt/projetos', pt_projects_path + + get '/pt/projetos/1/editar' + assert_equal 'projects#edit', @response.body + assert_equal '/pt/projetos/1/editar', edit_pt_project_path(1) + + get '/pt/administrador' + assert_equal 'admins#show', @response.body + assert_equal '/pt/administrador', pt_admin_path + + get '/pt/administrador/novo' + assert_equal 'admins#new', @response.body + assert_equal '/pt/administrador/novo', new_pt_admin_path + + put '/pt/administrador/ativar' + assert_equal 'admins#activate', @response.body + assert_equal '/pt/administrador/ativar', activate_pt_admin_path + end + + def test_path_option_override + draw do + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do + put :close, :on => :member, :path => 'fechar' + get :open, :on => :new, :path => 'abrir' + end + end + end + + get '/pt/projetos/novo/abrir' + assert_equal 'projects#open', @response.body + assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path + + put '/pt/projetos/1/fechar' + assert_equal 'projects#close', @response.body + assert_equal '/pt/projetos/1/fechar', close_pt_project_path(1) + end + + def test_sprockets + draw do + get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp + end + + get '/sprockets.js' + assert_equal 'javascripts', @response.body + end + + def test_update_person_route + draw do + get 'people/:id/update', :to => 'people#update', :as => :update_person + end + + get '/people/1/update' + assert_equal 'people#update', @response.body + + assert_equal '/people/1/update', update_person_path(:id => 1) + end + + def test_update_project_person + draw do + get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person + end + + get '/projects/1/people/2/update' + assert_equal 'people#update', @response.body + + assert_equal '/projects/1/people/2/update', update_project_person_path(:project_id => 1, :id => 2) + end + + def test_forum_products + draw do + namespace :forum do + resources :products, :path => '' do + resources :questions + end + end + end + + get '/forum' + assert_equal 'forum/products#index', @response.body + assert_equal '/forum', forum_products_path + + get '/forum/basecamp' + assert_equal 'forum/products#show', @response.body + assert_equal '/forum/basecamp', forum_product_path(:id => 'basecamp') + + get '/forum/basecamp/questions' + assert_equal 'forum/questions#index', @response.body + assert_equal '/forum/basecamp/questions', forum_product_questions_path(:product_id => 'basecamp') + + get '/forum/basecamp/questions/1' + assert_equal 'forum/questions#show', @response.body + assert_equal '/forum/basecamp/questions/1', forum_product_question_path(:product_id => 'basecamp', :id => 1) + end + + def test_articles_perma + draw do + get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article + end + + get '/articles/2009/08/18/rails-3' + assert_equal 'articles#show', @response.body + + assert_equal '/articles/2009/8/18/rails-3', article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') + end + + def test_account_namespace + draw do + namespace :account do + resource :subscription, :credit, :credit_card + end + end + + get '/account/subscription' + assert_equal 'account/subscriptions#show', @response.body + assert_equal '/account/subscription', account_subscription_path + + get '/account/credit' + assert_equal 'account/credits#show', @response.body + assert_equal '/account/credit', account_credit_path + + get '/account/credit_card' + assert_equal 'account/credit_cards#show', @response.body + assert_equal '/account/credit_card', account_credit_card_path + end + + def test_nested_namespace + draw do + namespace :account do + namespace :admin do + resource :subscription + end + end + end + + get '/account/admin/subscription' + assert_equal 'account/admin/subscriptions#show', @response.body + assert_equal '/account/admin/subscription', account_admin_subscription_path + end + + def test_namespace_nested_in_resources + draw do + resources :clients do + namespace :google do + resource :account do + namespace :secret do + resource :info + end + end + end + end + end + + get '/clients/1/google/account' + assert_equal '/clients/1/google/account', client_google_account_path(1) + assert_equal 'google/accounts#show', @response.body + + get '/clients/1/google/account/secret/info' + assert_equal '/clients/1/google/account/secret/info', client_google_account_secret_info_path(1) + assert_equal 'google/secret/infos#show', @response.body + end + + def test_namespace_with_options + draw do + namespace :users, :path => 'usuarios' do + root :to => 'home#index' + end + end + + get '/usuarios' + assert_equal '/usuarios', users_root_path + assert_equal 'users/home#index', @response.body + end + + def test_namespaced_shallow_routes_with_module_option + draw do + namespace :foo, module: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'bar/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'bar/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'bar/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', foo_comment_path('2') + assert_equal 'bar/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_path_option + draw do + namespace :foo, path: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/bar/posts' + assert_equal '/bar/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/bar/posts/1' + assert_equal '/bar/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/bar/posts/1/comments' + assert_equal '/bar/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/bar/comments/2' + assert_equal '/bar/comments/2', foo_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_as_option + draw do + namespace :foo, as: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', bar_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', bar_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', bar_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', bar_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_shallow_path_option + draw do + namespace :foo, shallow_path: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/bar/comments/2' + assert_equal '/bar/comments/2', foo_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespaced_shallow_routes_with_shallow_prefix_option + draw do + namespace :foo, shallow_prefix: 'bar' do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get '/foo/posts' + assert_equal '/foo/posts', foo_posts_path + assert_equal 'foo/posts#index', @response.body + + get '/foo/posts/1' + assert_equal '/foo/posts/1', foo_post_path('1') + assert_equal 'foo/posts#show', @response.body + + get '/foo/posts/1/comments' + assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') + assert_equal 'foo/comments#index', @response.body + + get '/foo/comments/2' + assert_equal '/foo/comments/2', bar_comment_path('2') + assert_equal 'foo/comments#show', @response.body + end + + def test_namespace_containing_numbers + draw do + namespace :v2 do + resources :subscriptions + end + end + + get '/v2/subscriptions' + assert_equal 'v2/subscriptions#index', @response.body + assert_equal '/v2/subscriptions', v2_subscriptions_path + end + + def test_articles_with_id + draw do + controller :articles do + scope '/articles', :as => 'article' do + scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do + get '/:id', :action => :with_id, :as => "" + end + end + end + end + + get '/articles/rails/1' + assert_equal 'articles#with_id', @response.body + + get '/articles/123/1' + assert_equal 'pass', @response.headers['X-Cascade'] + + assert_equal '/articles/rails/1', article_with_title_path(:title => 'rails', :id => 1) + end + + def test_access_token_rooms + draw do + scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do + resources :rooms + end + end + + 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 + + def test_root + draw do + root :to => 'projects#index' + end + + assert_equal '/', root_path + get '/' + assert_equal 'projects#index', @response.body + end + + def test_scoped_root + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index' + end + end + + assert_equal '/en', root_path(:locale => 'en') + get '/en' + assert_equal 'projects#index', @response.body + end + + def test_scoped_root_as_name + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index', :as => 'projects' + end + end + + assert_equal '/en', projects_path(:locale => 'en') + assert_equal '/', projects_path + get '/en' + assert_equal 'projects#index', @response.body + end + + def test_scope_with_format_option + draw do + get "direct/index", as: :no_format_direct, format: false + + scope format: false do + get "scoped/index", as: :no_format_scoped + end + end + + assert_equal "/direct/index", no_format_direct_path + assert_equal "/direct/index?format=html", no_format_direct_path(format: "html") + + assert_equal "/scoped/index", no_format_scoped_path + assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html") + + get '/scoped/index' + assert_equal "scoped#index", @response.body + + get '/scoped/index.html' + assert_equal "Not Found", @response.body + end + + def test_resources_with_format_false_from_scope + draw do + scope format: false do + resources :posts + resource :user + end + end + + get "/posts" + assert_response :success + assert_equal "posts#index", @response.body + assert_equal "/posts", posts_path + + get "/posts.html" + assert_response :not_found + assert_equal "Not Found", @response.body + assert_equal "/posts?format=html", posts_path(format: "html") + + get "/user" + assert_response :success + assert_equal "users#show", @response.body + assert_equal "/user", user_path + + get "/user.html" + assert_response :not_found + assert_equal "Not Found", @response.body + assert_equal "/user?format=html", user_path(format: "html") + end + + def test_index + draw do + get '/info' => 'projects#info', :as => 'info' + end + + assert_equal '/info', info_path + get '/info' + assert_equal 'projects#info', @response.body + end + + def test_match_with_many_paths_containing_a_slash + draw do + get 'get/first', 'get/second', 'get/third', :to => 'get#show' + end + + get '/get/first' + assert_equal 'get#show', @response.body + + get '/get/second' + assert_equal 'get#show', @response.body + + get '/get/third' + assert_equal 'get#show', @response.body + end + + def test_match_shorthand_with_no_scope + draw do + get 'account/overview' + end + + assert_equal '/account/overview', account_overview_path + get '/account/overview' + assert_equal 'account#overview', @response.body + end + + def test_match_shorthand_inside_namespace + draw do + namespace :account do + get 'shorthand' + end + end + + assert_equal '/account/shorthand', account_shorthand_path + get '/account/shorthand' + assert_equal 'account#shorthand', @response.body + end + + def test_match_shorthand_with_multiple_paths_inside_namespace + draw do + namespace :proposals do + put 'activate', 'inactivate' + end + end + + put '/proposals/activate' + assert_equal 'proposals#activate', @response.body + + put '/proposals/inactivate' + assert_equal 'proposals#inactivate', @response.body + end + + def test_match_shorthand_inside_namespace_with_controller + draw do + namespace :api do + get "products/list" + end + end + + assert_equal '/api/products/list', api_products_list_path + get '/api/products/list' + assert_equal 'api/products#list', @response.body + end + + def test_match_shorthand_inside_scope_with_variables_with_controller + draw do + scope ':locale' do + match 'questions/new', via: [:get] + end + end + + get '/de/questions/new' + assert_equal 'questions#new', @response.body + assert_equal 'de', @request.params[:locale] + end + + def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller + draw do + namespace :api do + namespace :v3 do + scope ':locale' do + get "products/list" + end + end + end + end + + get '/api/v3/en/products/list' + assert_equal 'api/v3/products#list', @response.body + end + + def test_controller_option_with_nesting_and_leading_slash + draw do + scope '/job', controller: 'job' do + scope ':id', action: 'manage_applicant' do + get "/active" + end + end + end + + get '/job/5/active' + assert_equal 'job#manage_applicant', @response.body + end + + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + draw do + resources :replies do + collection do + get 'page/:page' => 'replies#index', :page => %r{\d+} + get ':page' => 'replies#index', :page => %r{\d+} + end + end + end + + assert_equal '/replies', replies_path + end + + def test_scoped_controller_with_namespace_and_action + draw do + namespace :account do + get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback + end + end + + assert_equal '/account/twitter/callback', account_callback_path("twitter") + get '/account/twitter/callback' + assert_equal 'account/callbacks#twitter', @response.body + + get '/account/whatever/callback' + assert_equal 'Not Found', @response.body + end + + def test_convention_match_nested_and_with_leading_slash + draw do + get '/account/nested/overview' + end + + assert_equal '/account/nested/overview', account_nested_overview_path + get '/account/nested/overview' + assert_equal 'account/nested#overview', @response.body + end + + def test_convention_with_explicit_end + draw do + get 'sign_in' => "sessions#new" + end + + get '/sign_in' + assert_equal 'sessions#new', @response.body + assert_equal '/sign_in', sign_in_path + end + + def test_redirect_with_complete_url_and_status + draw do + get 'account/google' => redirect('http://www.google.com/', :status => 302) + end + + get '/account/google' + verify_redirect 'http://www.google.com/', 302 + end + + def test_redirect_with_port + draw do + get 'account/login', :to => redirect("/login") + end + + previous_host, self.host = self.host, 'www.example.com:3000' + + get '/account/login' + verify_redirect 'http://www.example.com:3000/login' + ensure + self.host = previous_host + end + + def test_normalize_namespaced_matches + draw do + namespace :account do + get 'description', :action => :description, :as => "description" + end + end + + assert_equal '/account/description', account_description_path + + get '/account/description' + assert_equal 'account#description', @response.body + end + + def test_namespaced_roots + draw do + namespace :account do + root :to => "account#index" + end + end + + assert_equal '/account', account_root_path + get '/account' + assert_equal 'account/account#index', @response.body + end + + def test_optional_scoped_root + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index' + end + end + + assert_equal '/en', root_path("en") + get '/en' + assert_equal 'projects#index', @response.body + end + + def test_optional_scoped_path + draw do + scope '(:locale)', :locale => /en|pl/ do + resources :descriptions + end + end + + assert_equal '/en/descriptions', descriptions_path("en") + assert_equal '/descriptions', descriptions_path(nil) + assert_equal '/en/descriptions/1', description_path("en", 1) + assert_equal '/descriptions/1', description_path(nil, 1) + + get '/en/descriptions' + assert_equal 'descriptions#index', @response.body + + get '/descriptions' + assert_equal 'descriptions#index', @response.body + + get '/en/descriptions/1' + assert_equal 'descriptions#show', @response.body + + get '/descriptions/1' + assert_equal 'descriptions#show', @response.body + end + + def test_nested_optional_scoped_path + draw do + namespace :admin do + scope '(:locale)', :locale => /en|pl/ do + resources :descriptions + end + end + end + + assert_equal '/admin/en/descriptions', admin_descriptions_path("en") + assert_equal '/admin/descriptions', admin_descriptions_path(nil) + assert_equal '/admin/en/descriptions/1', admin_description_path("en", 1) + assert_equal '/admin/descriptions/1', admin_description_path(nil, 1) + + get '/admin/en/descriptions' + assert_equal 'admin/descriptions#index', @response.body + + get '/admin/descriptions' + assert_equal 'admin/descriptions#index', @response.body + + get '/admin/en/descriptions/1' + assert_equal 'admin/descriptions#show', @response.body + + get '/admin/descriptions/1' + assert_equal 'admin/descriptions#show', @response.body + end + + def test_nested_optional_path_shorthand + draw do + scope '(:locale)', :locale => /en|pl/ do + get "registrations/new" + end + end + + get '/registrations/new' + assert_nil @request.params[:locale] + + get '/en/registrations/new' + assert_equal 'en', @request.params[:locale] + end + + def test_default_string_params + draw do + get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home' + get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' } + + defaults :id => 'home' do + get 'scoped_pages/(:id)', :to => 'pages#show' + end + end + + get '/inline_pages' + assert_equal 'home', @request.params[:id] + + get '/default_pages' + assert_equal 'home', @request.params[:id] + + get '/scoped_pages' + assert_equal 'home', @request.params[:id] + end + + def test_default_integer_params + draw do + get 'inline_pages/(:page)', to: 'pages#show', page: 1 + get 'default_pages/(:page)', to: 'pages#show', defaults: { page: 1 } + + defaults page: 1 do + get 'scoped_pages/(:page)', to: 'pages#show' + end + end + + get '/inline_pages' + assert_equal 1, @request.params[:page] + + get '/default_pages' + assert_equal 1, @request.params[:page] + + get '/scoped_pages' + assert_equal 1, @request.params[:page] + end + + def test_resource_constraints + draw do + resources :products, :constraints => { :id => /\d{4}/ } do + root :to => "products#root" + get :favorite, :on => :collection + resources :images + end + + resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } + end + + get '/products/1' + assert_equal 'pass', @response.headers['X-Cascade'] + get '/products' + assert_equal 'products#root', @response.body + get '/products/favorite' + assert_equal 'products#favorite', @response.body + get '/products/0001' + assert_equal 'products#show', @response.body + + get '/products/1/images' + assert_equal 'pass', @response.headers['X-Cascade'] + get '/products/0001/images' + assert_equal 'images#index', @response.body + get '/products/0001/images/0001' + assert_equal 'images#show', @response.body + + get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + get '/dashboard', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'dashboards#show', @response.body + end + + def test_root_works_in_the_resources_scope + draw do + resources :products do + root :to => "products#root" + end + end + + get '/products' + assert_equal 'products#root', @response.body + assert_equal '/products', products_root_path + end + + def test_module_scope + draw do + resource :token, :module => :api + end + + get '/token' + assert_equal 'api/tokens#show', @response.body + assert_equal '/token', token_path + end + + def test_path_scope + draw do + scope :path => 'api' do + resource :me + get '/' => 'mes#index' + end + end + + get '/api/me' + assert_equal 'mes#show', @response.body + assert_equal '/api/me', me_path + + get '/api' + assert_equal 'mes#index', @response.body + end + + def test_symbol_scope + draw do + scope :path => 'api' do + scope :v2 do + resource :me, as: 'v2_me' + get '/' => 'mes#index' + end + + scope :v3, :admin do + resource :me, as: 'v3_me' + end + end + end + + get '/api/v2/me' + assert_equal 'mes#show', @response.body + assert_equal '/api/v2/me', v2_me_path + + get '/api/v2' + assert_equal 'mes#index', @response.body + + get '/api/v3/admin/me' + assert_equal 'mes#show', @response.body + end + + def test_url_generator_for_generic_route + draw do + get "whatever/:controller(/:action(/:id))" + end + + get '/whatever/foo/bar' + assert_equal 'foo#bar', @response.body + + assert_equal 'http://www.example.com/whatever/foo/bar/1', + url_for(:controller => "foo", :action => "bar", :id => 1) + end + + def test_url_generator_for_namespaced_generic_route + draw do + get "whatever/:controller(/:action(/:id))", :id => /\d+/ + end + + get '/whatever/foo/bar/show' + assert_equal 'foo/bar#show', @response.body + + get '/whatever/foo/bar/show/1' + assert_equal 'foo/bar#show', @response.body + + assert_equal 'http://www.example.com/whatever/foo/bar/show', + url_for(:controller => "foo/bar", :action => "show") + + assert_equal 'http://www.example.com/whatever/foo/bar/show/1', + url_for(:controller => "foo/bar", :action => "show", :id => '1') + end + + def test_resource_new_actions + draw do + resources :replies do + new do + post :preview + end + end + + scope 'pt', :as => 'pt' do + resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do + post :preview, :on => :new + end + + resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do + post :preview, :on => :new + end + + resources :products, :path_names => { :new => 'novo' } do + new do + post :preview + end + end + end + + resource :profile do + new do + post :preview + end + end + end + + assert_equal '/replies/new/preview', preview_new_reply_path + assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path + assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path + assert_equal '/pt/products/novo/preview', preview_new_pt_product_path + assert_equal '/profile/new/preview', preview_new_profile_path + + post '/replies/new/preview' + assert_equal 'replies#preview', @response.body + + post '/pt/projetos/novo/preview' + assert_equal 'projects#preview', @response.body + + post '/pt/administrador/novo/preview' + assert_equal 'admins#preview', @response.body + + post '/pt/products/novo/preview' + assert_equal 'products#preview', @response.body + + post '/profile/new/preview' + assert_equal 'profiles#preview', @response.body + end + + def test_resource_merges_options_from_scope + draw do + scope :only => :show do + resource :account + end + end + + assert_raise(NoMethodError) { new_account_path } + + get '/account/new' + assert_equal 404, status + end + + def test_resources_merges_options_from_scope + draw do + scope :only => [:index, :show] do + resources :products do + resources :images + end + end + end + + assert_raise(NoMethodError) { edit_product_path('1') } + + get '/products/1/edit' + assert_equal 404, status + + assert_raise(NoMethodError) { edit_product_image_path('1', '2') } + + post '/products/1/images/2/edit' + assert_equal 404, status + end + + def test_shallow_nested_resources + draw do + shallow do + namespace :api do + resources :teams do + resources :players + resource :captain + end + end + end + + resources :threads, :shallow => true do + resource :owner + resources :messages do + resources :comments do + member do + post :preview + end + end + end + end + end + + get '/api/teams' + assert_equal 'api/teams#index', @response.body + assert_equal '/api/teams', api_teams_path + + get '/api/teams/new' + assert_equal 'api/teams#new', @response.body + assert_equal '/api/teams/new', new_api_team_path + + get '/api/teams/1' + assert_equal 'api/teams#show', @response.body + assert_equal '/api/teams/1', api_team_path(:id => '1') + + get '/api/teams/1/edit' + assert_equal 'api/teams#edit', @response.body + assert_equal '/api/teams/1/edit', edit_api_team_path(:id => '1') + + get '/api/teams/1/players' + assert_equal 'api/players#index', @response.body + assert_equal '/api/teams/1/players', api_team_players_path(:team_id => '1') + + get '/api/teams/1/players/new' + assert_equal 'api/players#new', @response.body + assert_equal '/api/teams/1/players/new', new_api_team_player_path(:team_id => '1') + + get '/api/players/2' + assert_equal 'api/players#show', @response.body + assert_equal '/api/players/2', api_player_path(:id => '2') + + get '/api/players/2/edit' + assert_equal 'api/players#edit', @response.body + assert_equal '/api/players/2/edit', edit_api_player_path(:id => '2') + + get '/api/teams/1/captain' + assert_equal 'api/captains#show', @response.body + assert_equal '/api/teams/1/captain', api_team_captain_path(:team_id => '1') + + get '/api/teams/1/captain/new' + assert_equal 'api/captains#new', @response.body + assert_equal '/api/teams/1/captain/new', new_api_team_captain_path(:team_id => '1') + + get '/api/teams/1/captain/edit' + assert_equal 'api/captains#edit', @response.body + assert_equal '/api/teams/1/captain/edit', edit_api_team_captain_path(:team_id => '1') + + get '/threads' + assert_equal 'threads#index', @response.body + assert_equal '/threads', threads_path + + get '/threads/new' + assert_equal 'threads#new', @response.body + assert_equal '/threads/new', new_thread_path + + get '/threads/1' + assert_equal 'threads#show', @response.body + assert_equal '/threads/1', thread_path(:id => '1') + + get '/threads/1/edit' + assert_equal 'threads#edit', @response.body + assert_equal '/threads/1/edit', edit_thread_path(:id => '1') + + get '/threads/1/owner' + assert_equal 'owners#show', @response.body + assert_equal '/threads/1/owner', thread_owner_path(:thread_id => '1') + + get '/threads/1/messages' + assert_equal 'messages#index', @response.body + assert_equal '/threads/1/messages', thread_messages_path(:thread_id => '1') + + get '/threads/1/messages/new' + assert_equal 'messages#new', @response.body + assert_equal '/threads/1/messages/new', new_thread_message_path(:thread_id => '1') + + get '/messages/2' + assert_equal 'messages#show', @response.body + assert_equal '/messages/2', message_path(:id => '2') + + get '/messages/2/edit' + assert_equal 'messages#edit', @response.body + assert_equal '/messages/2/edit', edit_message_path(:id => '2') + + get '/messages/2/comments' + assert_equal 'comments#index', @response.body + assert_equal '/messages/2/comments', message_comments_path(:message_id => '2') + + get '/messages/2/comments/new' + assert_equal 'comments#new', @response.body + assert_equal '/messages/2/comments/new', new_message_comment_path(:message_id => '2') + + get '/comments/3' + assert_equal 'comments#show', @response.body + assert_equal '/comments/3', comment_path(:id => '3') + + get '/comments/3/edit' + assert_equal 'comments#edit', @response.body + assert_equal '/comments/3/edit', edit_comment_path(:id => '3') + + post '/comments/3/preview' + assert_equal 'comments#preview', @response.body + assert_equal '/comments/3/preview', preview_comment_path(:id => '3') + end + + def test_shallow_nested_resources_inside_resource + draw do + resource :membership, shallow: true do + resources :cards + end + end + + get '/membership/cards' + assert_equal 'cards#index', @response.body + assert_equal '/membership/cards', membership_cards_path + + get '/membership/cards/new' + assert_equal 'cards#new', @response.body + assert_equal '/membership/cards/new', new_membership_card_path + + post '/membership/cards' + assert_equal 'cards#create', @response.body + + get '/cards/1' + assert_equal 'cards#show', @response.body + assert_equal '/cards/1', card_path('1') + + get '/cards/1/edit' + assert_equal 'cards#edit', @response.body + assert_equal '/cards/1/edit', edit_card_path('1') + + put '/cards/1' + assert_equal 'cards#update', @response.body + + patch '/cards/1' + assert_equal 'cards#update', @response.body + + delete '/cards/1' + assert_equal 'cards#destroy', @response.body + end + + def test_shallow_deeply_nested_resources + draw do + resources :blogs do + resources :posts do + resources :comments, shallow: true + end + end + end + + get '/comments/1' + assert_equal 'comments#show', @response.body + + assert_equal '/comments/1', comment_path('1') + assert_equal '/blogs/new', new_blog_path + assert_equal '/blogs/1/posts/new', new_blog_post_path(:blog_id => 1) + assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2) + end + + def test_shallow_nested_resources_within_scope + draw do + scope '/hello' do + shallow do + resources :notes do + resources :trackbacks + end + end + end + end + + get '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#index', @response.body + assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) + + get '/hello/notes/1/edit' + assert_equal 'notes#edit', @response.body + assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') + + get '/hello/notes/1/trackbacks/new' + assert_equal 'trackbacks#new', @response.body + assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) + + get '/hello/trackbacks/1' + assert_equal 'trackbacks#show', @response.body + assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') + + get '/hello/trackbacks/1/edit' + assert_equal 'trackbacks#edit', @response.body + assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') + + put '/hello/trackbacks/1' + assert_equal 'trackbacks#update', @response.body + + post '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#create', @response.body + + delete '/hello/trackbacks/1' + assert_equal 'trackbacks#destroy', @response.body + + get '/hello/notes' + assert_equal 'notes#index', @response.body + + post '/hello/notes' + assert_equal 'notes#create', @response.body + + get '/hello/notes/new' + assert_equal 'notes#new', @response.body + assert_equal '/hello/notes/new', new_note_path + + get '/hello/notes/1' + assert_equal 'notes#show', @response.body + assert_equal '/hello/notes/1', note_path(:id => 1) + + put '/hello/notes/1' + assert_equal 'notes#update', @response.body + + delete '/hello/notes/1' + assert_equal 'notes#destroy', @response.body + end + + def test_shallow_option_nested_resources_within_scope + draw do + scope '/hello' do + resources :notes, :shallow => true do + resources :trackbacks + end + end + end + + get '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#index', @response.body + assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) + + get '/hello/notes/1/edit' + assert_equal 'notes#edit', @response.body + assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') + + get '/hello/notes/1/trackbacks/new' + assert_equal 'trackbacks#new', @response.body + assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) + + get '/hello/trackbacks/1' + assert_equal 'trackbacks#show', @response.body + assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') + + get '/hello/trackbacks/1/edit' + assert_equal 'trackbacks#edit', @response.body + assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') + + put '/hello/trackbacks/1' + assert_equal 'trackbacks#update', @response.body + + post '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#create', @response.body + + delete '/hello/trackbacks/1' + assert_equal 'trackbacks#destroy', @response.body + + get '/hello/notes' + assert_equal 'notes#index', @response.body + + post '/hello/notes' + assert_equal 'notes#create', @response.body + + get '/hello/notes/new' + assert_equal 'notes#new', @response.body + assert_equal '/hello/notes/new', new_note_path + + get '/hello/notes/1' + assert_equal 'notes#show', @response.body + assert_equal '/hello/notes/1', note_path(:id => 1) + + put '/hello/notes/1' + assert_equal 'notes#update', @response.body + + delete '/hello/notes/1' + assert_equal 'notes#destroy', @response.body + end + + def test_custom_resource_routes_are_scoped + draw do + resources :customers do + get :recent, :on => :collection + get "profile", :on => :member + get "secret/profile" => "customers#secret", :on => :member + post "preview" => "customers#preview", :as => :another_preview, :on => :new + resource :avatar do + get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member + end + resources :invoices do + get "outstanding" => "invoices#outstanding", :on => :collection + get "overdue", :action => :overdue, :on => :collection + get "print" => "invoices#print", :as => :print, :on => :member + post "preview" => "invoices#preview", :as => :preview, :on => :new + end + resources :notes, :shallow => true do + get "preview" => "notes#preview", :as => :preview, :on => :new + get "print" => "notes#print", :as => :print, :on => :member + end + end + + namespace :api do + resources :customers do + get "recent" => "customers#recent", :as => :recent, :on => :collection + get "profile" => "customers#profile", :as => :profile, :on => :member + post "preview" => "customers#preview", :as => :preview, :on => :new + end + end + end + + assert_equal '/customers/recent', recent_customers_path + assert_equal '/customers/1/profile', profile_customer_path(:id => '1') + assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1') + assert_equal '/customers/new/preview', another_preview_new_customer_path + assert_equal '/customers/1/avatar/thumbnail.jpg', thumbnail_customer_avatar_path(:customer_id => '1', :format => :jpg) + assert_equal '/customers/1/invoices/outstanding', outstanding_customer_invoices_path(:customer_id => '1') + assert_equal '/customers/1/invoices/2/print', print_customer_invoice_path(:customer_id => '1', :id => '2') + assert_equal '/customers/1/invoices/new/preview', preview_new_customer_invoice_path(:customer_id => '1') + assert_equal '/customers/1/notes/new/preview', preview_new_customer_note_path(:customer_id => '1') + assert_equal '/notes/1/print', print_note_path(:id => '1') + assert_equal '/api/customers/recent', recent_api_customers_path + assert_equal '/api/customers/1/profile', profile_api_customer_path(:id => '1') + assert_equal '/api/customers/new/preview', preview_new_api_customer_path + + get '/customers/1/invoices/overdue' + assert_equal 'invoices#overdue', @response.body + + get '/customers/1/secret/profile' + assert_equal 'customers#secret', @response.body + end + + def test_shallow_nested_routes_ignore_module + draw do + scope :module => :api do + resources :errors, :shallow => true do + resources :notices + end + end + end + + get '/errors/1/notices' + assert_equal 'api/notices#index', @response.body + assert_equal '/errors/1/notices', error_notices_path(:error_id => '1') + + get '/notices/1' + assert_equal 'api/notices#show', @response.body + assert_equal '/notices/1', notice_path(:id => '1') + end + + def test_non_greedy_regexp + draw do + namespace :api do + scope(':version', :version => /.+/) do + resources :users, :id => /.+?/, :format => /json|xml/ + end + end + end + + get '/api/1.0/users' + assert_equal 'api/users#index', @response.body + assert_equal '/api/1.0/users', api_users_path(:version => '1.0') + + get '/api/1.0/users.json' + assert_equal 'api/users#index', @response.body + assert_equal true, @request.format.json? + assert_equal '/api/1.0/users.json', api_users_path(:version => '1.0', :format => :json) + + get '/api/1.0/users/first.last' + assert_equal 'api/users#show', @response.body + assert_equal 'first.last', @request.params[:id] + assert_equal '/api/1.0/users/first.last', api_user_path(:version => '1.0', :id => 'first.last') + + get '/api/1.0/users/first.last.xml' + assert_equal 'api/users#show', @response.body + assert_equal 'first.last', @request.params[:id] + assert_equal true, @request.format.xml? + assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml) + end + + def test_match_without_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show' + end + end + end + + def test_match_with_empty_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show', :via => [] + end + end + end + + def test_glob_parameter_accepts_regexp + draw do + get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ + end + + get '/en/path/to/existing/file.html' + assert_equal 200, @response.status + end + + def test_resources_controller_name_is_not_pluralized + draw do + resources :content + end + + get '/content' + assert_equal 'content#index', @response.body + end + + def test_url_generator_for_optional_prefix_dynamic_segment + draw do + get "(/:username)/followers" => "followers#index" + end + + get '/bob/followers' + assert_equal 'followers#index', @response.body + assert_equal 'http://www.example.com/bob/followers', + url_for(:controller => "followers", :action => "index", :username => "bob") + + get '/followers' + assert_equal 'followers#index', @response.body + assert_equal 'http://www.example.com/followers', + url_for(:controller => "followers", :action => "index", :username => nil) + end + + def test_url_generator_for_optional_suffix_static_and_dynamic_segment + draw do + get "/groups(/user/:username)" => "groups#index" + end + + get '/groups/user/bob' + assert_equal 'groups#index', @response.body + assert_equal 'http://www.example.com/groups/user/bob', + url_for(:controller => "groups", :action => "index", :username => "bob") + + get '/groups' + assert_equal 'groups#index', @response.body + assert_equal 'http://www.example.com/groups', + url_for(:controller => "groups", :action => "index", :username => nil) + end + + def test_url_generator_for_optional_prefix_static_and_dynamic_segment + draw do + get "(/user/:username)/photos" => "photos#index" + end + + get '/user/bob/photos' + assert_equal 'photos#index', @response.body + assert_equal 'http://www.example.com/user/bob/photos', + url_for(:controller => "photos", :action => "index", :username => "bob") + + get '/photos' + assert_equal 'photos#index', @response.body + assert_equal 'http://www.example.com/photos', + url_for(:controller => "photos", :action => "index", :username => nil) + end + + def test_url_recognition_for_optional_static_segments + draw do + scope '(groups)' do + scope '(discussions)' do + resources :messages + end + end + end + + get '/groups/discussions/messages' + assert_equal 'messages#index', @response.body + + get '/groups/discussions/messages/1' + assert_equal 'messages#show', @response.body + + get '/groups/messages' + assert_equal 'messages#index', @response.body + + get '/groups/messages/1' + assert_equal 'messages#show', @response.body + + get '/discussions/messages' + assert_equal 'messages#index', @response.body + + get '/discussions/messages/1' + assert_equal 'messages#show', @response.body + + get '/messages' + assert_equal 'messages#index', @response.body + + get '/messages/1' + assert_equal 'messages#show', @response.body + end + + def test_router_removes_invalid_conditions + draw do + scope :constraints => { :id => /\d+/ } do + get '/tickets', :to => 'tickets#index', :as => :tickets + end + end + + get '/tickets' + assert_equal 'tickets#index', @response.body + assert_equal '/tickets', tickets_path + end + + def test_constraints_are_merged_from_scope + draw do + scope :constraints => { :id => /\d{4}/ } do + resources :movies do + resources :reviews + resource :trailer + end + end + end + + get '/movies/0001' + assert_equal 'movies#show', @response.body + assert_equal '/movies/0001', movie_path(:id => '0001') + + get '/movies/00001' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::UrlGenerationError){ movie_path(:id => '00001') } + + get '/movies/0001/reviews' + assert_equal 'reviews#index', @response.body + assert_equal '/movies/0001/reviews', movie_reviews_path(:movie_id => '0001') + + get '/movies/00001/reviews' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::UrlGenerationError){ movie_reviews_path(:movie_id => '00001') } + + get '/movies/0001/reviews/0001' + assert_equal 'reviews#show', @response.body + assert_equal '/movies/0001/reviews/0001', movie_review_path(:movie_id => '0001', :id => '0001') + + get '/movies/00001/reviews/0001' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::UrlGenerationError){ movie_path(:movie_id => '00001', :id => '00001') } + + get '/movies/0001/trailer' + assert_equal 'trailers#show', @response.body + assert_equal '/movies/0001/trailer', movie_trailer_path(:movie_id => '0001') + + get '/movies/00001/trailer' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::UrlGenerationError){ movie_trailer_path(:movie_id => '00001') } + end + + def test_only_should_be_read_from_scope + draw do + scope :only => [:index, :show] do + namespace :only do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + + get '/only/clubs' + assert_equal 'only/clubs#index', @response.body + assert_equal '/only/clubs', only_clubs_path + + get '/only/clubs/1/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_path(:id => '1') } + + get '/only/clubs/1/players' + assert_equal 'only/players#index', @response.body + assert_equal '/only/clubs/1/players', only_club_players_path(:club_id => '1') + + get '/only/clubs/1/players/2/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_player_path(:club_id => '1', :id => '2') } + + get '/only/clubs/1/chairman' + assert_equal 'only/chairmen#show', @response.body + assert_equal '/only/clubs/1/chairman', only_club_chairman_path(:club_id => '1') + + get '/only/clubs/1/chairman/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_chairman_path(:club_id => '1') } + end + + def test_except_should_be_read_from_scope + draw do + scope :except => [:new, :create, :edit, :update, :destroy] do + namespace :except do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + + get '/except/clubs' + assert_equal 'except/clubs#index', @response.body + assert_equal '/except/clubs', except_clubs_path + + get '/except/clubs/1/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_path(:id => '1') } + + get '/except/clubs/1/players' + assert_equal 'except/players#index', @response.body + assert_equal '/except/clubs/1/players', except_club_players_path(:club_id => '1') + + get '/except/clubs/1/players/2/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_player_path(:club_id => '1', :id => '2') } + + get '/except/clubs/1/chairman' + assert_equal 'except/chairmen#show', @response.body + assert_equal '/except/clubs/1/chairman', except_club_chairman_path(:club_id => '1') + + get '/except/clubs/1/chairman/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_chairman_path(:club_id => '1') } + end + + def test_only_option_should_override_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index + end + end + end + + get '/only/sectors' + assert_equal 'only/sectors#index', @response.body + assert_equal '/only/sectors', only_sectors_path + + get '/only/sectors/1' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_path(:id => '1') } + end + + def test_only_option_should_not_inherit + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies + resource :leader + end + end + end + end + + get '/only/sectors/1/companies/2' + assert_equal 'only/companies#show', @response.body + assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2') + + get '/only/sectors/1/leader' + assert_equal 'only/leaders#show', @response.body + assert_equal '/only/sectors/1/leader', only_sector_leader_path(:sector_id => '1') + end + + def test_except_option_should_override_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] + end + end + end + + get '/except/sectors' + assert_equal 'except/sectors#index', @response.body + assert_equal '/except/sectors', except_sectors_path + + get '/except/sectors/1' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_path(:id => '1') } + end + + def test_except_option_should_not_inherit + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies + resource :leader + end + end + end + end + + get '/except/sectors/1/companies/2' + assert_equal 'except/companies#show', @response.body + assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2') + + get '/except/sectors/1/leader' + assert_equal 'except/leaders#show', @response.body + assert_equal '/except/sectors/1/leader', except_sector_leader_path(:sector_id => '1') + end + + def test_except_option_should_override_scoped_only + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :managers, :except => [:show, :update, :destroy] + end + end + end + end + + get '/only/sectors/1/managers' + assert_equal 'only/managers#index', @response.body + assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1') + + get '/only/sectors/1/managers/2' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_manager_path(:sector_id => '1', :id => '2') } + end + + def test_only_option_should_override_scoped_except + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :managers, :only => :index + end + end + end + end + + get '/except/sectors/1/managers' + assert_equal 'except/managers#index', @response.body + assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1') + + get '/except/sectors/1/managers/2' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_manager_path(:sector_id => '1', :id => '2') } + end + + def test_only_scope_should_override_parent_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies do + scope :only => :index do + resources :divisions + end + end + end + end + end + end + + get '/only/sectors/1/companies/2/divisions' + assert_equal 'only/divisions#index', @response.body + assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2') + + get '/only/sectors/1/companies/2/divisions/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + + def test_except_scope_should_override_parent_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies do + scope :except => [:show, :update, :destroy] do + resources :divisions + end + end + end + end + end + end + + get '/except/sectors/1/companies/2/divisions' + assert_equal 'except/divisions#index', @response.body + assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2') + + get '/except/sectors/1/companies/2/divisions/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + + def test_except_scope_should_override_parent_only_scope + draw do + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies do + scope :except => [:show, :update, :destroy] do + resources :departments + end + end + end + end + end + end + + get '/only/sectors/1/companies/2/departments' + assert_equal 'only/departments#index', @response.body + assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2') + + get '/only/sectors/1/companies/2/departments/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + + def test_only_scope_should_override_parent_except_scope + draw do + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies do + scope :only => :index do + resources :departments + end + end + end + end + end + end + + get '/except/sectors/1/companies/2/departments' + assert_equal 'except/departments#index', @response.body + assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2') + + get '/except/sectors/1/companies/2/departments/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + + def test_resources_are_not_pluralized + draw do + namespace :transport do + resources :taxis + end + end + + get '/transport/taxis' + assert_equal 'transport/taxis#index', @response.body + assert_equal '/transport/taxis', transport_taxis_path + + get '/transport/taxis/new' + assert_equal 'transport/taxis#new', @response.body + assert_equal '/transport/taxis/new', new_transport_taxi_path + + post '/transport/taxis' + assert_equal 'transport/taxis#create', @response.body + + get '/transport/taxis/1' + assert_equal 'transport/taxis#show', @response.body + assert_equal '/transport/taxis/1', transport_taxi_path(:id => '1') + + get '/transport/taxis/1/edit' + assert_equal 'transport/taxis#edit', @response.body + assert_equal '/transport/taxis/1/edit', edit_transport_taxi_path(:id => '1') + + put '/transport/taxis/1' + assert_equal 'transport/taxis#update', @response.body + + delete '/transport/taxis/1' + assert_equal 'transport/taxis#destroy', @response.body + end + + def test_singleton_resources_are_not_singularized + draw do + namespace :medical do + resource :taxis + end + end + + get '/medical/taxis/new' + assert_equal 'medical/taxis#new', @response.body + assert_equal '/medical/taxis/new', new_medical_taxis_path + + post '/medical/taxis' + assert_equal 'medical/taxis#create', @response.body + + get '/medical/taxis' + assert_equal 'medical/taxis#show', @response.body + assert_equal '/medical/taxis', medical_taxis_path + + get '/medical/taxis/edit' + assert_equal 'medical/taxis#edit', @response.body + assert_equal '/medical/taxis/edit', edit_medical_taxis_path + + put '/medical/taxis' + assert_equal 'medical/taxis#update', @response.body + + delete '/medical/taxis' + assert_equal 'medical/taxis#destroy', @response.body + end + + def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action + draw do + resources :sections, :id => /.+/ do + get :preview, :on => :member + end + end + + get '/sections/1/edit' + assert_equal 'sections#edit', @response.body + assert_equal '/sections/1/edit', edit_section_path(:id => '1') + + get '/sections/1/preview' + assert_equal 'sections#preview', @response.body + assert_equal '/sections/1/preview', preview_section_path(:id => '1') + end + + def test_resource_constraints_are_pushed_to_scope + draw do + namespace :wiki do + resources :articles, :id => /[^\/]+/ do + resources :comments, :only => [:create, :new] + end + end + end + + get '/wiki/articles/Ruby_on_Rails_3.0' + assert_equal 'wiki/articles#show', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0') + + get '/wiki/articles/Ruby_on_Rails_3.0/comments/new' + assert_equal 'wiki/comments#new', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments/new', new_wiki_article_comment_path(:article_id => 'Ruby_on_Rails_3.0') + + post '/wiki/articles/Ruby_on_Rails_3.0/comments' + assert_equal 'wiki/comments#create', @response.body + assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments', wiki_article_comments_path(:article_id => 'Ruby_on_Rails_3.0') + end + + def test_resources_path_can_be_a_symbol + draw do + resources :wiki_pages, :path => :pages + resource :wiki_account, :path => :my_account + end + + get '/pages' + assert_equal 'wiki_pages#index', @response.body + assert_equal '/pages', wiki_pages_path + + get '/pages/Ruby_on_Rails' + assert_equal 'wiki_pages#show', @response.body + assert_equal '/pages/Ruby_on_Rails', wiki_page_path(:id => 'Ruby_on_Rails') + + get '/my_account' + assert_equal 'wiki_accounts#show', @response.body + assert_equal '/my_account', wiki_account_path + end + + def test_redirect_https + draw do + get 'secure', :to => redirect("/secure/login") + end + + with_https do + get '/secure' + verify_redirect 'https://www.example.com/secure/login' + end + end + + def test_symbolized_path_parameters_is_not_stale + draw do + scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do + get '/', :to => 'countries#index' + get '/cities', :to => 'countries#cities' + end + + get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } + end + + get '/countries/France' + assert_equal 'countries#index', @response.body + + get '/countries/France/cities' + assert_equal 'countries#cities', @response.body + + get '/countries/UK' + verify_redirect 'http://www.example.com/countries/all' + + get '/countries/UK/cities' + verify_redirect 'http://www.example.com/countries/all/cities' + end + + def test_constraints_block_not_carried_to_following_routes + draw do + scope '/italians' do + get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor + get '/sculptors', :to => 'italians#sculptors' + get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/} + end + end + + get '/italians/writers' + assert_equal 'Not Found', @response.body + + get '/italians/sculptors' + assert_equal 'italians#sculptors', @response.body + + get '/italians/painters/botticelli' + assert_equal 'Not Found', @response.body + + get '/italians/painters/michelangelo' + assert_equal 'italians#painters', @response.body + end + + def test_custom_resource_actions_defined_using_string + draw do + resources :customers do + resources :invoices do + get "aged/:months", :on => :collection, :action => :aged, :as => :aged + end + + get "inactive", :on => :collection + post "deactivate", :on => :member + get "old", :on => :collection, :as => :stale + end + end + + get '/customers/inactive' + assert_equal 'customers#inactive', @response.body + assert_equal '/customers/inactive', inactive_customers_path + + post '/customers/1/deactivate' + assert_equal 'customers#deactivate', @response.body + assert_equal '/customers/1/deactivate', deactivate_customer_path(:id => '1') + + get '/customers/old' + assert_equal 'customers#old', @response.body + assert_equal '/customers/old', stale_customers_path + + get '/customers/1/invoices/aged/3' + assert_equal 'invoices#aged', @response.body + assert_equal '/customers/1/invoices/aged/3', aged_customer_invoices_path(:customer_id => '1', :months => '3') + end + + def test_route_defined_in_resources_scope_level + draw do + resources :customers do + get "export" + end + end + + get '/customers/1/export' + assert_equal 'customers#export', @response.body + assert_equal '/customers/1/export', customer_export_path(:customer_id => '1') + end + + def test_named_character_classes_in_regexp_constraints + draw do + get '/purchases/:token/:filename', + :to => 'purchases#fetch', + :token => /[[:alnum:]]{10}/, + :filename => /(.+)/, + :as => :purchase + end + + get '/purchases/315004be7e/Ruby_on_Rails_3.pdf' + assert_equal 'purchases#fetch', @response.body + assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf') + end + + def test_nested_resource_constraints + draw do + resources :lists, :id => /([A-Za-z0-9]{25})|default/ do + resources :todos, :id => /\d+/ + end + end + + get '/lists/01234012340123401234fffff' + assert_equal 'lists#show', @response.body + assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff') + + get '/lists/01234012340123401234fffff/todos/1' + assert_equal 'todos#show', @response.body + assert_equal '/lists/01234012340123401234fffff/todos/1', list_todo_path(:list_id => '01234012340123401234fffff', :id => '1') + + get '/lists/2/todos/1' + assert_equal 'Not Found', @response.body + assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') } + end + + def test_redirect_argument_error + routes = Class.new { include ActionDispatch::Routing::Redirection }.new + assert_raises(ArgumentError) { routes.redirect Object.new } + end + + def test_named_route_check + before, after = nil + + draw do + before = has_named_route?(:hello) + get "/hello", as: :hello, to: "hello#world" + after = has_named_route?(:hello) + end + + assert !before, "expected to not have named route :hello before route definition" + assert after, "expected to have named route :hello after route definition" + end + + def test_explicitly_avoiding_the_named_route + draw do + scope :as => "routes" do + get "/c/:id", :as => :collision, :to => "collision#show" + get "/collision", :to => "collision#show" + get "/no_collision", :to => "collision#show", :as => nil + end + end + + assert !respond_to?(:routes_no_collision_path) + end + + def test_controller_name_with_leading_slash_raise_error + assert_raise(ArgumentError) do + draw { get '/feeds/:service', :to => '/feeds#show' } + end + + assert_raise(ArgumentError) do + draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' } + end + + assert_raise(ArgumentError) do + draw { get '/api/feeds/:service', :to => '/api/feeds#show' } + end + + assert_raise(ArgumentError) do + assert_deprecated do + draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + end + end + + assert_raise(ArgumentError) do + draw { resources :feeds, :controller => '/feeds' } + end + end + + def test_invalid_route_name_raises_error + assert_raise(ArgumentError) do + draw { get '/products', :to => 'products#index', :as => 'products ' } + end + + assert_raise(ArgumentError) do + draw { get '/products', :to => 'products#index', :as => ' products' } + end + + assert_raise(ArgumentError) do + draw { get '/products', :to => 'products#index', :as => 'products!' } + end + + assert_raise(ArgumentError) do + draw { get '/products', :to => 'products#index', :as => 'products index' } + end + + assert_raise(ArgumentError) do + draw { get '/products', :to => 'products#index', :as => '1products' } + end + end + + def test_duplicate_route_name_raises_error + assert_raise(ArgumentError) do + draw do + get '/collision', :to => 'collision#show', :as => 'collision' + get '/duplicate', :to => 'duplicate#show', :as => 'collision' + end + end + end + + def test_duplicate_route_name_via_resources_raises_error + assert_raise(ArgumentError) do + draw do + resources :collisions + get '/collision', :to => 'collision#show', :as => 'collision' + end + end + end + + def test_nested_route_in_nested_resource + draw do + resources :posts, :only => [:index, :show] do + resources :comments, :except => :destroy do + get "views" => "comments#views", :as => :views + end + end + end + + get "/posts/1/comments/2/views" + assert_equal "comments#views", @response.body + assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2') + end + + def test_root_in_deeply_nested_scope + draw do + resources :posts, :only => [:index, :show] do + namespace :admin do + root :to => "index#index" + end + end + end + + get "/posts/1/admin" + assert_equal "admin/index#index", @response.body + assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1') + end + + def test_custom_param + draw do + resources :profiles, :param => :username do + get :details, :on => :member + resources :messages + end + end + + get '/profiles/bob' + assert_equal 'profiles#show', @response.body + assert_equal 'bob', @request.params[:username] + + get '/profiles/bob/details' + assert_equal 'bob', @request.params[:username] + + get '/profiles/bob/messages/34' + assert_equal 'bob', @request.params[:profile_username] + assert_equal '34', @request.params[:id] + end + + def test_custom_param_constraint + draw do + resources :profiles, :param => :username, :username => /[a-z]+/ do + get :details, :on => :member + resources :messages + end + end + + get '/profiles/bob1' + assert_equal 404, @response.status + + get '/profiles/bob1/details' + assert_equal 404, @response.status + + get '/profiles/bob1/messages/34' + assert_equal 404, @response.status + end + + def test_shallow_custom_param + draw do + resources :orders do + constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do + resources :downloads, :param => :download, :shallow => true + end + end + end + + get '/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip' + assert_equal 'downloads#show', @response.body + assert_equal '0c0c0b68-d24b-11e1-a861-001ff3fffe6f', @request.params[:download] + end + + def test_action_from_path_is_not_frozen + draw do + get 'search' => 'search' + end + + get '/search' + assert !@request.params[:action].frozen? + end + + def test_multiple_positional_args_with_the_same_name + draw do + get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false + end + + expected_params = { + controller: 'downloads', + action: 'show', + id: '1' + } + + get '/downloads/1/1.tar' + assert_equal 'downloads#show', @response.body + assert_equal expected_params, @request.symbolized_path_parameters + assert_equal '/downloads/1/1.tar', download_path('1') + assert_equal '/downloads/1/1.tar', download_path('1', '1') + end + + def test_absolute_controller_namespace + draw do + namespace :foo do + get '/', to: '/bar#index', as: 'root' + end + end + + get '/foo' + assert_equal 'bar#index', @response.body + assert_equal '/foo', foo_root_path + end + + def test_namespace_as_controller + draw do + namespace :foo do + get '/', to: '/bar#index', as: 'root' + end + end + + get '/foo' + assert_equal 'bar#index', @response.body + assert_equal '/foo', foo_root_path + end + + def test_trailing_slash + draw do + resources :streams + end + + get '/streams' + assert @response.ok?, 'route without trailing slash should work' + + get '/streams/' + assert @response.ok?, 'route with trailing slash should work' + + get '/streams?foobar' + assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work' + + get '/streams/?foobar' + assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work' + end + + def test_route_with_dashes_in_path + draw do + get '/contact-us', to: 'pages#contact_us' + end + + get '/contact-us' + assert_equal 'pages#contact_us', @response.body + assert_equal '/contact-us', contact_us_path + end + + def test_shorthand_route_with_dashes_in_path + draw do + get '/about-us/index' + end + + get '/about-us/index' + assert_equal 'about_us#index', @response.body + assert_equal '/about-us/index', about_us_index_path + end + + def test_resource_routes_with_dashes_in_path + draw do + resources :photos, only: [:show] do + get 'user-favorites', on: :collection + get 'preview-photo', on: :member + get 'summary-text' + end + end + + get '/photos/user-favorites' + assert_equal 'photos#user_favorites', @response.body + assert_equal '/photos/user-favorites', user_favorites_photos_path + + get '/photos/1/preview-photo' + assert_equal 'photos#preview_photo', @response.body + assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1') + + get '/photos/1/summary-text' + assert_equal 'photos#summary_text', @response.body + assert_equal '/photos/1/summary-text', photo_summary_text_path('1') + + get '/photos/1' + assert_equal 'photos#show', @response.body + assert_equal '/photos/1', photo_path('1') + end + + def test_shallow_path_inside_namespace_is_not_added_twice + draw do + namespace :admin do + shallow do + resources :posts do + resources :comments + end + end + end + end + + get '/admin/posts/1/comments' + assert_equal 'admin/comments#index', @response.body + assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') + end + + def test_mix_string_to_controller_action + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_controller + draw do + get '/projects', controller: 'project_files', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_action + draw do + get '/projects', action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_symbol_to_controller_action + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: :show + end + end + get '/projects' + assert_equal 'project_files#show', @response.body + end + + def test_mix_string_to_controller_action_no_hash + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'show' + end + end + get '/projects' + assert_equal 'show#index', @response.body + end + + def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes + draw do + scope shallow_path: 'projects', shallow_prefix: 'project' do + resources :projects do + resources :files, controller: 'project_files', shallow: true + end + end + end + + get '/projects' + assert_equal 'projects#index', @response.body + assert_equal '/projects', projects_path + + get '/projects/new' + assert_equal 'projects#new', @response.body + assert_equal '/projects/new', new_project_path + + post '/projects' + assert_equal 'projects#create', @response.body + + get '/projects/1' + assert_equal 'projects#show', @response.body + assert_equal '/projects/1', project_path('1') + + get '/projects/1/edit' + assert_equal 'projects#edit', @response.body + assert_equal '/projects/1/edit', edit_project_path('1') + + patch '/projects/1' + assert_equal 'projects#update', @response.body + + delete '/projects/1' + assert_equal 'projects#destroy', @response.body + + get '/projects/1/files' + assert_equal 'project_files#index', @response.body + assert_equal '/projects/1/files', project_files_path('1') + + get '/projects/1/files/new' + assert_equal 'project_files#new', @response.body + assert_equal '/projects/1/files/new', new_project_file_path('1') + + post '/projects/1/files' + assert_equal 'project_files#create', @response.body + + get '/projects/files/2' + assert_equal 'project_files#show', @response.body + assert_equal '/projects/files/2', project_file_path('2') + + get '/projects/files/2/edit' + assert_equal 'project_files#edit', @response.body + assert_equal '/projects/files/2/edit', edit_project_file_path('2') + + patch '/projects/files/2' + assert_equal 'project_files#update', @response.body + + delete '/projects/files/2' + assert_equal 'project_files#destroy', @response.body + end + + def test_scope_path_is_copied_to_shallow_path + draw do + scope path: 'foo' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/foo/comments/1', comment_path('1') + end + + def test_scope_as_is_copied_to_shallow_prefix + draw do + scope as: 'foo' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/comments/1', foo_comment_path('1') + end + + def test_scope_shallow_prefix_is_not_overwritten_by_as + draw do + scope as: 'foo', shallow_prefix: 'bar' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/comments/1', bar_comment_path('1') + end + + def test_scope_shallow_path_is_not_overwritten_by_path + draw do + scope path: 'foo', shallow_path: 'bar' do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal '/bar/comments/1', comment_path('1') + end + +private + + def draw(&block) + self.class.stub_controllers do |routes| + @app = routes + @app.default_url_options = { host: 'www.example.com' } + @app.draw(&block) + end + end + + def url_for(options = {}) + @app.url_helpers.url_for(options) + end + + def method_missing(method, *args, &block) + if method.to_s =~ /_(path|url)$/ + @app.url_helpers.send(method, *args, &block) + else + super + end + end + + def with_https + old_https = https? + https! + yield + ensure + https!(old_https) + end + + def verify_redirect(url, status=301) + assert_equal status, @response.status + assert_equal url, @response.headers['Location'] + assert_equal expected_redirect_body(url), @response.body + end + + def expected_redirect_body(url) + %(You are being redirected.) + end +end + +class TestAltApp < ActionDispatch::IntegrationTest + class AltRequest + attr_accessor :path_parameters, :path_info, :script_name + attr_reader :env + + def initialize(env) + @path_parameters = {} + @env = env + @path_info = "/" + @script_name = "" + end + + def request_method + "GET" + end + + def ip + "127.0.0.1" + end + + def x_header + @env["HTTP_X_HEADER"] || "" + end + end + + class XHeader + def call(env) + [200, {"Content-Type" => "text/html"}, ["XHeader"]] + end + end + + class AltApp + def call(env) + [200, {"Content-Type" => "text/html"}, ["Alternative App"]] + end + end + + AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) + AltRoutes.draw do + get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} + get "/" => TestAltApp::AltApp.new + end + + def app + AltRoutes + end + + def test_alt_request_without_header + get "/" + assert_equal "Alternative App", @response.body + end + + def test_alt_request_with_matched_header + get "/", {}, "HTTP_X_HEADER" => "HEADER" + assert_equal "XHeader", @response.body + end + + def test_alt_request_with_unmatched_header + get "/", {}, "HTTP_X_HEADER" => "NON_MATCH" + assert_equal "Alternative App", @response.body + end +end + +class TestAppendingRoutes < ActionDispatch::IntegrationTest + def simple_app(resp) + lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] } + end + + def setup + super + s = self + @app = ActionDispatch::Routing::RouteSet.new + @app.append do + get '/hello' => s.simple_app('fail') + get '/goodbye' => s.simple_app('goodbye') + end + + @app.draw do + get '/hello' => s.simple_app('hello') + end + end + + def test_goodbye_should_be_available + get '/goodbye' + assert_equal 'goodbye', @response.body + end + + def test_hello_should_not_be_overwritten + get '/hello' + assert_equal 'hello', @response.body + end + + def test_missing_routes_are_still_missing + get '/random' + assert_equal 404, @response.status + end +end + +class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest + module ::Admin + class StorageFilesController < ActionController::Base + def index + render :text => "admin/storage_files#index" + end + end + end + + def draw(&block) + @app = ActionDispatch::Routing::RouteSet.new + @app.draw(&block) + end + + def test_missing_controller + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :action => :index + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_action + ex = assert_raises(ArgumentError) { + assert_deprecated do + draw do + get '/foo/bar', :to => 'foo' + end + end + } + assert_match(/Missing :action/, ex.message) + end + + def test_missing_action_on_hash + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => 'foo#' + end + } + assert_match(/Missing :action/, ex.message) + end + + def test_valid_controller_options_inside_namespace + draw do + namespace :admin do + resources :storage_files, :controller => "storage_files" + end + end + + get '/admin/storage_files' + assert_equal "admin/storage_files#index", @response.body + end + + def test_resources_with_valid_namespaced_controller_option + draw do + resources :storage_files, :controller => 'admin/storage_files' + end + + get '/storage_files' + assert_equal "admin/storage_files#index", @response.body + end + + def test_warn_with_ruby_constant_syntax_controller_option + e = assert_raise(ArgumentError) do + draw do + namespace :admin do + resources :storage_files, :controller => "StorageFiles" + end + end + end + + assert_match "'admin/StorageFiles' is not a supported controller name", e.message + end + + def test_warn_with_ruby_constant_syntax_namespaced_controller_option + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, :controller => 'Admin::StorageFiles' + end + end + + assert_match "'Admin::StorageFiles' is not a supported controller name", e.message + end + + def test_warn_with_ruby_constant_syntax_no_colons + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, :controller => 'Admin' + end + end + + assert_match "'Admin' is not a supported controller name", e.message + end +end + +class TestDefaultScope < ActionDispatch::IntegrationTest + module ::Blog + class PostsController < ActionController::Base + def index + render :text => "blog/posts#index" + end + end + end + + DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new + DefaultScopeRoutes.default_scope = {:module => :blog} + DefaultScopeRoutes.draw do + resources :posts + end + + def app + DefaultScopeRoutes + end + + include DefaultScopeRoutes.url_helpers + + def test_default_scope + get '/posts' + assert_equal "blog/posts#index", @response.body + end +end + +class TestHttpMethods < ActionDispatch::IntegrationTest + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) + RFC5789 = %w(PATCH) + + def simple_app(response) + lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } + end + + setup do + s = self + @app = ActionDispatch::Routing::RouteSet.new + + @app.draw do + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| + match '/' => s.simple_app(method), :via => method.underscore.to_sym + end + end + end + + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| + test "request method #{method.underscore} can be matched" do + get '/', nil, 'REQUEST_METHOD' => method + assert_equal method, @response.body + end + end +end + +class TestUriPathEscaping < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get '/:segment' => lambda { |env| + path_params = env['action_dispatch.request.path_parameters'] + [200, { 'Content-Type' => 'text/plain' }, [path_params[:segment]]] + }, :as => :segment + + get '/*splat' => lambda { |env| + path_params = env['action_dispatch.request.path_parameters'] + [200, { 'Content-Type' => 'text/plain' }, [path_params[:splat]]] + }, :as => :splat + end + end + + include Routes.url_helpers + def app; Routes end + + test 'escapes slash in generated path segment' do + assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d') + end + + test 'unescapes recognized path segment' do + get '/a%20b%2Fc+d' + assert_equal 'a b/c+d', @response.body + end + + test 'does not escape slash in generated path splat' do + assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d') + end + + test 'unescapes recognized path splat' do + get '/a%20b/c+d' + assert_equal 'a b/c+d', @response.body + end +end + +class TestUnicodePaths < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/ほげ" => lambda { |env| + [200, { 'Content-Type' => 'text/plain' }, []] + }, :as => :unicode_path + end + end + + include Routes.url_helpers + def app; Routes end + + test 'recognizes unicode path' do + get "/#{Rack::Utils.escape("ほげ")}" + assert_equal "200", @response.code + end +end + +class TestMultipleNestedController < ActionDispatch::IntegrationTest + module ::Foo + module Bar + class BazController < ActionController::Base + def index + render :inline => "<%= url_for :controller => '/pooh', :action => 'index' %>" + end + end + end + end + + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + namespace :foo do + namespace :bar do + get "baz" => "baz#index" + end + end + get "pooh" => "pooh#index" + end + end + + include Routes.url_helpers + def app; Routes end + + test "controller option which starts with '/' from multiple nested controller" do + get "/foo/bar/baz" + assert_equal "/pooh", @response.body + end +end + +class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/~user" => ok + get "/young-and-fine" => ok + end + end + + include Routes.url_helpers + def app; Routes end + + test 'recognizes tilde path' do + get "/~user" + assert_equal "200", @response.code + end + + test 'recognizes minus path' do + get "/young-and-fine" + assert_equal "200", @response.code + end + +end + +class TestRedirectInterpolation < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/foo/:id" => redirect("/foo/bar/%{id}") + get "/bar/:id" => redirect(:path => "/foo/bar/%{id}") + get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}") + get "/foo/bar/:id" => ok + get "/baz" => ok + end + end + + def app; Routes end + + test "redirect escapes interpolated parameters with redirect proc" do + get "/foo/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + + test "redirect escapes interpolated parameters with option proc" do + get "/bar/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + + test "path redirect escapes interpolated parameters correctly" do + get "/foo/1%201" + verify_redirect "http://www.example.com/foo/bar/1%201" + + get "/baz/1%201" + verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201" + end + +private + def verify_redirect(url, status=301) + assert_equal status, @response.status + assert_equal url, @response.headers['Location'] + assert_equal expected_redirect_body(url), @response.body + end + + def expected_redirect_body(url) + %(You are being redirected.) + end +end + +class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == 'foo' } + get "/:bar" => ok + end + end + + def app; Routes end + + test "parameters are reset between constraint checks" do + get "/bar" + assert_equal nil, @request.params[:foo] + assert_equal "bar", @request.params[:bar] + end +end + +class TestGlobRoutingMapper < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get "/*id" => redirect("/not_cars"), :constraints => {id: /dummy/} + get "/cars" => ok + end + end + + #include Routes.url_helpers + def app; Routes end + + def test_glob_constraint + get "/dummy" + assert_equal "301", @response.code + assert_equal "/not_cars", @response.header['Location'].match('/[^/]+$')[0] + end + + def test_glob_constraint_skip_route + get "/cars" + assert_equal "200", @response.code + end + def test_glob_constraint_skip_all + get "/missing" + assert_equal "404", @response.code + end +end + +class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/foo' => ok, as: :foo + get '/post(/:action(/:id))' => ok, as: :posts + get '/:foo/:foo_type/bars/:id' => ok, as: :bar + get '/projects/:id.:format' => ok, as: :project + get '/pages/:id' => ok, as: :page + get '/wiki/*page' => ok, as: :wiki + end + end + + include Routes.url_helpers + def app; Routes end + + test 'enabled when not mounted and default_url_options is empty' do + assert Routes.url_helpers.optimize_routes_generation? + end + + test 'named route called as singleton method' do + assert_equal '/foo', Routes.url_helpers.foo_path + end + + test 'named route called on included module' do + assert_equal '/foo', foo_path + end + + test 'nested optional segments are removed' do + assert_equal '/post', Routes.url_helpers.posts_path + assert_equal '/post', posts_path + end + + test 'segments with same prefix are replaced correctly' do + assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1') + assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1') + end + + test 'segments separated with a period are replaced correctly' do + assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json) + assert_equal '/projects/1.json', project_path(1, :json) + end + + test 'segments with question marks are escaped' do + assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar') + assert_equal '/pages/foo%3Fbar', page_path('foo?bar') + end + + test 'segments with slashes are escaped' do + assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar') + assert_equal '/pages/foo%2Fbar', page_path('foo/bar') + end + + test 'glob segments with question marks are escaped' do + assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar') + assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar') + end + + test 'glob segments with slashes are not escaped' do + assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar') + assert_equal '/wiki/foo/bar', wiki_path('foo/bar') + end +end + +class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest + class CategoriesController < ActionController::Base + def show + render :text => "categories#show" + end + end + + class ProductsController < ActionController::Base + def show + render :text => "products#show" + end + end + + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + scope :module => "test_named_route_url_helpers" do + get "/categories/:id" => 'categories#show', :as => :category + get "/products/:id" => 'products#show', :as => :product + end + end + end + + def app; Routes end + + include Routes.url_helpers + + test "url helpers do not ignore nil parameters when using non-optimized routes" do + Routes.stubs(:optimize_routes_generation?).returns(false) + + get "/categories/1" + assert_response :success + assert_raises(ActionController::UrlGenerationError) { product_path(nil) } + end +end + +class TestUrlConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + constraints :subdomain => 'admin' do + get '/' => ok, :as => :admin_root + end + + scope :constraints => { :protocol => 'https://' } do + get '/' => ok, :as => :secure_root + end + + get '/' => ok, :as => :alternate_root, :constraints => { :port => 8080 } + + get '/search' => ok, :constraints => { :subdomain => false } + + get '/logs' => ok, :constraints => { :subdomain => true } + end + end + + include Routes.url_helpers + def app; Routes end + + test "constraints are copied to defaults when using constraints method" do + assert_equal 'http://admin.example.com/', admin_root_url + + get 'http://admin.example.com/' + assert_response :success + end + + test "constraints are copied to defaults when using scope constraints hash" do + assert_equal 'https://www.example.com/', secure_root_url + + get 'https://www.example.com/' + assert_response :success + end + + test "constraints are copied to defaults when using route constraints hash" do + assert_equal 'http://www.example.com:8080/', alternate_root_url + + get 'http://www.example.com:8080/' + assert_response :success + end + + test "false constraint expressions check for absence of values" do + get 'http://example.com/search' + assert_response :success + assert_equal 'http://example.com/search', search_url + + get 'http://api.example.com/search' + assert_response :not_found + end + + test "true constraint expressions check for presence of values" do + get 'http://api.example.com/logs' + assert_response :success + assert_equal 'http://api.example.com/logs', logs_url + + get 'http://example.com/logs' + assert_response :not_found + end +end + +class TestInvalidUrls < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def show + render :text => "foo#show" + end + end + + test "invalid UTF-8 encoding returns a 400 Bad Request" do + with_routing do |set| + set.draw do + get "/bar/:id", :to => redirect("/foo/show/%{id}") + get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" + get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" + get "/:controller(/:action(/:id))" + end + + get "/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/foo/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/foo/show/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/bar/%E2%EF%BF%BD%A6" + assert_response :bad_request + end + end +end + +class TestOptionalRootSegments < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + get '/(page/:page)', :to => 'pages#index', :as => :root + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_optional_root_segments + get '/' + assert_equal 'pages#index', @response.body + assert_equal '/', root_path + + get '/page/1' + assert_equal 'pages#index', @response.body + assert_equal '1', @request.params[:page] + assert_equal '/page/1', root_path('1') + assert_equal '/page/1', root_path(:page => '1') + end +end + +class TestPortConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get '/integer', to: ok, constraints: { :port => 8080 } + get '/string', to: ok, constraints: { :port => '8080' } + get '/array', to: ok, constraints: { :port => [8080] } + get '/regexp', to: ok, constraints: { :port => /8080/ } + end + end + + include Routes.url_helpers + def app; Routes end + + def test_integer_port_constraints + get 'http://www.example.com/integer' + assert_response :not_found + + get 'http://www.example.com:8080/integer' + assert_response :success + end + + def test_string_port_constraints + get 'http://www.example.com/string' + assert_response :not_found + + get 'http://www.example.com:8080/string' + assert_response :success + end + + def test_array_port_constraints + get 'http://www.example.com/array' + assert_response :not_found + + get 'http://www.example.com:8080/array' + assert_response :success + end + + def test_regexp_port_constraints + get 'http://www.example.com/regexp' + assert_response :not_found + + get 'http://www.example.com:8080/regexp' + assert_response :success + end +end + +class TestFormatConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + + get '/string', to: ok, constraints: { format: 'json' } + get '/regexp', to: ok, constraints: { format: /json/ } + get '/json_only', to: ok, format: true, constraints: { format: /json/ } + get '/xml_only', to: ok, format: 'xml' + end + end + + include Routes.url_helpers + def app; Routes end + + def test_string_format_constraints + get 'http://www.example.com/string' + assert_response :success + + get 'http://www.example.com/string.json' + assert_response :success + + get 'http://www.example.com/string.html' + assert_response :not_found + end + + def test_regexp_format_constraints + get 'http://www.example.com/regexp' + assert_response :success + + get 'http://www.example.com/regexp.json' + assert_response :success + + get 'http://www.example.com/regexp.html' + assert_response :not_found + end + + def test_enforce_with_format_true_with_constraint + get 'http://www.example.com/json_only.json' + assert_response :success + + get 'http://www.example.com/json_only.html' + assert_response :not_found + + get 'http://www.example.com/json_only' + assert_response :not_found + end + + def test_enforce_with_string + get 'http://www.example.com/xml_only.xml' + assert_response :success + + get 'http://www.example.com/xml_only' + assert_response :success + + get 'http://www.example.com/xml_only.json' + assert_response :not_found + end +end + +class TestCallableConstraintValidation < ActionDispatch::IntegrationTest + def test_constraint_with_object_not_callable + assert_raises(ArgumentError) do + ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/test', to: ok, constraints: Object.new + end + end + end + end +end + +class TestRouteDefaults < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + resources :posts, bucket_type: 'post' + resources :projects, defaults: { bucket_type: 'project' } + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_route_options_are_required_for_url_for + assert_raises(ActionController::UrlGenerationError) do + assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, only_path: true) + end + + assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, bucket_type: 'post', only_path: true) + end + + def test_route_defaults_are_not_required_for_url_for + assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true) + end +end + +class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + mount rack_app, at: '/account', as: 'account' + mount rack_app, at: '/:locale/account', as: 'localized_account' + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_mounted_application_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) + end + end +end + +class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + get '/account', to: redirect('/myaccount'), as: 'account' + get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account' + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_redirect_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) + end + end +end + +class TestUrlGenerationErrors < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/products/:id" => 'products#show', :as => :product + end + end + + def app; Routes end + + include Routes.url_helpers + + test "url helpers raise a helpful error message whem generation fails" do + url, missing = { action: 'show', controller: 'products', id: nil }, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } + assert_equal message, error.message + end +end From 38bcb5e6d1a10861010173ca771cf6b1a0afb657 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Fri, 15 Aug 2014 03:46:37 +0530 Subject: [PATCH 20/23] Implemented a basic singleton resource It declares routes for a simple resource :single_resource declaration --- .../action_dispatch/routing/dsl/resource.rb | 33 + .../lib/action_dispatch/routing/dsl/scope.rb | 1 + .../routing/dsl/scope/resources.rb | 221 + .../routing/dsl/singleton_resource.rb | 33 + actionpack/test/dispatch/new_routing_test.rb | 4218 +---------------- 5 files changed, 307 insertions(+), 4199 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/dsl/resource.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/scope/resources.rb create mode 100644 actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb diff --git a/actionpack/lib/action_dispatch/routing/dsl/resource.rb b/actionpack/lib/action_dispatch/routing/dsl/resource.rb new file mode 100644 index 0000000000000..233b5e436b973 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/resource.rb @@ -0,0 +1,33 @@ +module ActionDispatch + module Routing + module DSL + class Resource < Scope + # CANONICAL_ACTIONS holds all actions that does not need a prefix or + # a path appended since they fit properly in their scope level. + VALID_ON_OPTIONS = [:new, :collection, :member] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] + CANONICAL_ACTIONS = %w(index create new show update destroy) + + attr_reader :param + + def initialize(parent, resource, options) + super + @name = resource.to_s + @path = (options[:path] || @name).to_s + @controller = (options[:controller] || @name).to_s + @param = (options[:param] || :id).to_sym + + declare_resourceful_routes + end + + def name + @path + end + + def declare_resourceful_routes + + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope.rb b/actionpack/lib/action_dispatch/routing/dsl/scope.rb index 705264e47865e..7618dab386df5 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope.rb @@ -35,3 +35,4 @@ def has_named_route?(name) require 'action_dispatch/routing/dsl/scope/http_helpers' require 'action_dispatch/routing/dsl/scope/scoping' require 'action_dispatch/routing/dsl/scope/concerns' +require 'action_dispatch/routing/dsl/scope/resources' diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/resources.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/resources.rb new file mode 100644 index 0000000000000..d157a230caa72 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/resources.rb @@ -0,0 +1,221 @@ +require "action_dispatch/routing/dsl/resource" +require "action_dispatch/routing/dsl/singleton_resource" + +# Resource routing allows you to quickly declare all of the common routes +# for a given resourceful controller. Instead of declaring separate routes +# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ +# actions, a resourceful route declares them in a single line of code: +# +# resources :photos +# +# Sometimes, you have a resource that clients always look up without +# referencing an ID. A common example, /profile always shows the profile of +# the currently logged in user. In this case, you can use a singular resource +# to map /profile (rather than /profile/:id) to the show action. +# +# resource :profile +# +# It's common to have resources that are logically children of other +# resources: +# +# resources :magazines do +# resources :ads +# end +# +# You may wish to organize groups of controllers under a namespace. Most +# commonly, you might group a number of administrative controllers under +# an +admin+ namespace. You would place these controllers under the +# app/controllers/admin directory, and you can group them together +# in your router: +# +# namespace "admin" do +# resources :posts, :comments +# end +# +# By default the +:id+ parameter doesn't accept dots. If you need to +# use dots as part of the +:id+ parameter add a constraint which +# overrides this restriction, e.g: +# +# resources :articles, id: /[^\/]+/ +# +# This allows any character other than a slash as part of your +:id+. + +module ActionDispatch + module Routing + module DSL + class Scope + # In Rails, a resourceful route provides a mapping between HTTP verbs + # and URLs and controller actions. By convention, each action also maps + # to particular CRUD operations in a database. A single entry in the + # routing file, such as + # + # resources :photos + # + # creates seven different routes in your application, all mapping to + # the +Photos+ controller: + # + # GET /photos + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PATCH/PUT /photos/:id + # DELETE /photos/:id + # + # Resources can also be nested infinitely by using this block syntax: + # + # resources :photos do + # resources :comments + # end + # + # This generates the following comments routes: + # + # GET /photos/:photo_id/comments + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PATCH/PUT /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id + # + # === Options + # Takes same options as Base#match as well as: + # + # [:path_names] + # Allows you to change the segment component of the +edit+ and +new+ actions. + # Actions not specified are not changed. + # + # resources :posts, path_names: { new: "brand_new" } + # + # The above example will now change /posts/new to /posts/brand_new + # + # [:path] + # Allows you to change the path prefix for the resource. + # + # resources :posts, path: 'postings' + # + # The resource and all segments will now route to /postings instead of /posts + # + # [:only] + # Only generate routes for the given actions. + # + # resources :cows, only: :show + # resources :cows, only: [:show, :index] + # + # [:except] + # Generate all routes except for the given actions. + # + # resources :cows, except: :show + # resources :cows, except: [:show, :index] + # + # [:shallow] + # Generates shallow routes for nested resource(s). When placed on a parent resource, + # generates shallow routes for all nested resources. + # + # resources :posts, shallow: true do + # resources :comments + # end + # + # Is the same as: + # + # resources :posts do + # resources :comments, except: [:show, :edit, :update, :destroy] + # end + # resources :comments, only: [:show, :edit, :update, :destroy] + # + # This allows URLs for resources that otherwise would be deeply nested such + # as a comment on a blog post like /posts/a-long-permalink/comments/1234 + # to be shortened to just /comments/1234. + # + # [:shallow_path] + # Prefixes nested shallow routes with the specified path. + # + # scope shallow_path: "sekret" do + # resources :posts do + # resources :comments, shallow: true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PATCH/PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) + # + # [:shallow_prefix] + # Prefixes nested shallow route names with specified prefix. + # + # scope shallow_prefix: "sekret" do + # resources :posts do + # resources :comments, shallow: true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_sekret_comment GET /comments/:id/edit(.:format) + # sekret_comment GET /comments/:id(.:format) + # sekret_comment PATCH/PUT /comments/:id(.:format) + # sekret_comment DELETE /comments/:id(.:format) + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. + # + # === Examples + # + # # routes call Admin::PostsController + # resources :posts, module: "admin" + # + # # resource actions are at /admin/posts. + # resources :posts, path: "admin/posts" + def resources(*resources, &block) + common_behaviour_for Resource, *resources, &block + end + + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the + # profile of the currently logged in user. In this case, you can use + # a singular resource to map /profile (rather than /profile/:id) to + # the show action: + # + # resource :profile + # + # creates six different routes in your application, all mapping to + # the +Profiles+ controller (note that the controller is named after + # the plural): + # + # GET /profile/new + # POST /profile + # GET /profile + # GET /profile/edit + # PATCH/PUT /profile + # DELETE /profile + # + # === Options + # Takes same options as +resources+. + def resource(*resources, &block) + common_behaviour_for SingletonResource, *resources, &block + end + + private + def common_behaviour_for(klass, *resources, &block) + options = resources.extract_options!.dup + resources.each do |resource| + new_resource = klass.new(self, resource, options) + new_resource.instance_exec(&block) if block_given? + new_resource.draw + end + self + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb new file mode 100644 index 0000000000000..e80446aa00ee2 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb @@ -0,0 +1,33 @@ +module ActionDispatch + module Routing + module DSL + class SingletonResource < Scope + VALID_ON_OPTIONS = [:new, :collection, :member] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] + CANONICAL_ACTIONS = %w(index create new show update destroy) + RESOURCE_METHOD_SCOPES = [:collection, :member, :new] + + def initialize(parent, resource, options) + super + @name = resource.to_s + @path = (@path || @name).to_s + @controller = (@controller || @name).pluralize.to_s + end + + def name + as || @name + end + + def draw + post '/', action: :create, as: name + get '/new', action: :new, as: "new_#{name}" + get '/edit', action: :edit, as: "edit_#{name}" + get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy + end + end + end + end +end diff --git a/actionpack/test/dispatch/new_routing_test.rb b/actionpack/test/dispatch/new_routing_test.rb index 778dbfc74d2ae..0a3f0fbb953b2 100644 --- a/actionpack/test/dispatch/new_routing_test.rb +++ b/actionpack/test/dispatch/new_routing_test.rb @@ -143,3720 +143,36 @@ def test_session_singleton_resource assert_equal '/session/reset', reset_session_path end - def test_session_info_nested_singleton_resource - draw do - resource :session do - resource :info - end - end - - get '/session/info' - assert_equal 'infos#show', @response.body - assert_equal '/session/info', session_info_path - end - - def test_member_on_resource - draw do - resource :session do - member do - get :crush - end - end - end - - get '/session/crush' - assert_equal 'sessions#crush', @response.body - assert_equal '/session/crush', crush_session_path - end - - def test_redirect_modulo - draw do - get 'account/modulo/:name', :to => redirect("/%{name}s") - end - - get '/account/modulo/name' - verify_redirect 'http://www.example.com/names' - end - - def test_redirect_proc - draw do - get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" } - end - - get '/account/proc/person' - verify_redirect 'http://www.example.com/people' - end - - def test_redirect_proc_with_request - draw do - get 'account/proc_req' => redirect {|params, req| "/#{req.method}" } - end - - get '/account/proc_req' - verify_redirect 'http://www.example.com/GET' - end - - def test_redirect_hash_with_subdomain - draw do - get 'mobile', :to => redirect(:subdomain => 'mobile') - end - - get '/mobile' - verify_redirect 'http://mobile.example.com/mobile' - end - - def test_redirect_hash_with_domain_and_path - draw do - get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') - end - - get '/documentation' - verify_redirect 'http://www.example-documentation.com' - end - - def test_redirect_hash_with_path - draw do - get 'new_documentation', :to => redirect(:path => '/documentation/new') - end - - get '/new_documentation' - verify_redirect 'http://www.example.com/documentation/new' - end - - def test_redirect_hash_with_host - draw do - get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') - end - - get '/super_new_documentation?section=top' - verify_redirect 'http://super-docs.com/super_new_documentation?section=top' - end - - def test_redirect_hash_path_substitution - draw do - get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') - end - - get '/stores/iernest' - verify_redirect 'http://stores.example.com/iernest' - end - - def test_redirect_hash_path_substitution_with_catch_all - draw do - get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') - end - - get '/stores/iernest/products' - verify_redirect 'http://stores.example.com/iernest/products' - end - - def test_redirect_class - draw do - get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) - end - - get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' - verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' - end - - def test_openid - draw do - match 'openid/login', :via => [:get, :post], :to => "openid#login" - end - - get '/openid/login' - assert_equal 'openid#login', @response.body - - post '/openid/login' - assert_equal 'openid#login', @response.body - end - - def test_bookmarks - draw do - scope "bookmark", :controller => "bookmarks", :as => :bookmark do - get :new, :path => "build" - post :create, :path => "create", :as => "" - put :update - get :remove, :action => :destroy, :as => :remove - end - end - - get '/bookmark/build' - assert_equal 'bookmarks#new', @response.body - assert_equal '/bookmark/build', bookmark_new_path - - post '/bookmark/create' - assert_equal 'bookmarks#create', @response.body - assert_equal '/bookmark/create', bookmark_path - - put '/bookmark/update' - assert_equal 'bookmarks#update', @response.body - assert_equal '/bookmark/update', bookmark_update_path - - get '/bookmark/remove' - assert_equal 'bookmarks#destroy', @response.body - assert_equal '/bookmark/remove', bookmark_remove_path - end - - def test_pagemarks - draw do - scope "pagemark", :controller => "pagemarks", :as => :pagemark do - get "new", :path => "build" - post "create", :as => "" - put "update" - get "remove", :action => :destroy, :as => :remove - end - end - - get '/pagemark/build' - assert_equal 'pagemarks#new', @response.body - assert_equal '/pagemark/build', pagemark_new_path - - post '/pagemark/create' - assert_equal 'pagemarks#create', @response.body - assert_equal '/pagemark/create', pagemark_path - - put '/pagemark/update' - assert_equal 'pagemarks#update', @response.body - assert_equal '/pagemark/update', pagemark_update_path - - get '/pagemark/remove' - assert_equal 'pagemarks#destroy', @response.body - assert_equal '/pagemark/remove', pagemark_remove_path - end - - def test_admin - draw do - constraints(:ip => /192\.168\.1\.\d\d\d/) do - get 'admin' => "queenbee#index" - end - - constraints ::TestRoutingMapper::IpRestrictor do - get 'admin/accounts' => "queenbee#accounts" - end - - get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor - end - - get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} - assert_equal 'queenbee#index', @response.body - - get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} - assert_equal 'pass', @response.headers['X-Cascade'] - - get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} - assert_equal 'queenbee#accounts', @response.body - - get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} - assert_equal 'pass', @response.headers['X-Cascade'] - - get '/admin/passwords', {}, {'REMOTE_ADDR' => '192.168.1.100'} - assert_equal 'queenbee#passwords', @response.body - - get '/admin/passwords', {}, {'REMOTE_ADDR' => '10.0.0.100'} - assert_equal 'pass', @response.headers['X-Cascade'] - end - - def test_global - draw do - controller(:global) do - get 'global/hide_notice' - get 'global/export', :action => :export, :as => :export_request - get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } - get 'global/:action' - end - end - - 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', export_request_path - assert_equal '/global/hide_notice', global_hide_notice_path - assert_equal '/export/123/foo.txt', export_download_path(:id => 123, :file => 'foo.txt') - end - - def test_local - draw do - get "/local/:action", :controller => "local" - end - - get '/local/dashboard' - assert_equal 'local#dashboard', @response.body - end - - # tests the use of dup in url_for - def test_url_for_with_no_side_effects - draw do - get "/projects/status(.:format)" - end - - # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) - original_options = {:controller => 'projects', :action => 'status'} - options = original_options.dup - - url_for options - - # verify that the options passed in have not changed from the original ones - assert_equal original_options, options - end - - def test_url_for_does_not_modify_controller - draw do - get "/projects/status(.:format)" - end - - controller = '/projects' - options = {:controller => controller, :action => 'status', :only_path => true} - url = url_for(options) - - assert_equal '/projects/status', url - assert_equal '/projects', controller - end - - # tests the arguments modification free version of define_hash_access - def test_named_route_with_no_side_effects - draw do - resources :customers do - get "profile", :on => :member - end - end - - original_options = { :host => 'test.host' } - options = original_options.dup - - profile_customer_url("customer_model", options) - - # verify that the options passed in have not changed from the original ones - assert_equal original_options, options - end - - def test_projects_status - draw do - get "/projects/status(.:format)" - end - - assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true) - assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true) - end - - def test_projects - draw do - resources :projects, :controller => :project - end - - get '/projects' - assert_equal 'project#index', @response.body - assert_equal '/projects', projects_path - - post '/projects' - assert_equal 'project#create', @response.body - - get '/projects.xml' - assert_equal 'project#index', @response.body - assert_equal '/projects.xml', projects_path(:format => 'xml') - - get '/projects/new' - assert_equal 'project#new', @response.body - assert_equal '/projects/new', new_project_path - - get '/projects/new.xml' - assert_equal 'project#new', @response.body - assert_equal '/projects/new.xml', new_project_path(:format => 'xml') - - get '/projects/1' - assert_equal 'project#show', @response.body - assert_equal '/projects/1', project_path(:id => '1') - - get '/projects/1.xml' - assert_equal 'project#show', @response.body - assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml') - - get '/projects/1/edit' - assert_equal 'project#edit', @response.body - assert_equal '/projects/1/edit', edit_project_path(:id => '1') - end - - def test_projects_with_post_action_and_new_path_on_collection - draw do - resources :projects, :controller => :project do - post 'new', :action => 'new', :on => :collection, :as => :new - end - end - - post '/projects/new' - assert_equal "project#new", @response.body - assert_equal "/projects/new", new_projects_path - end - - def test_projects_involvements - draw do - resources :projects, :controller => :project do - resources :involvements, :attachments - end - end - - get '/projects/1/involvements' - assert_equal 'involvements#index', @response.body - assert_equal '/projects/1/involvements', project_involvements_path(:project_id => '1') - - get '/projects/1/involvements/new' - assert_equal 'involvements#new', @response.body - assert_equal '/projects/1/involvements/new', new_project_involvement_path(:project_id => '1') - - get '/projects/1/involvements/1' - assert_equal 'involvements#show', @response.body - assert_equal '/projects/1/involvements/1', project_involvement_path(:project_id => '1', :id => '1') - - put '/projects/1/involvements/1' - assert_equal 'involvements#update', @response.body - - delete '/projects/1/involvements/1' - assert_equal 'involvements#destroy', @response.body - - get '/projects/1/involvements/1/edit' - assert_equal 'involvements#edit', @response.body - assert_equal '/projects/1/involvements/1/edit', edit_project_involvement_path(:project_id => '1', :id => '1') - end - - def test_projects_attachments - draw do - resources :projects, :controller => :project do - resources :involvements, :attachments - end - end - - get '/projects/1/attachments' - assert_equal 'attachments#index', @response.body - assert_equal '/projects/1/attachments', project_attachments_path(:project_id => '1') - end - - def test_projects_participants - draw do - resources :projects, :controller => :project do - resources :participants do - put :update_all, :on => :collection - end - end - end - - get '/projects/1/participants' - assert_equal 'participants#index', @response.body - assert_equal '/projects/1/participants', project_participants_path(:project_id => '1') - - put '/projects/1/participants/update_all' - assert_equal 'participants#update_all', @response.body - assert_equal '/projects/1/participants/update_all', update_all_project_participants_path(:project_id => '1') - end - - def test_projects_companies - draw do - resources :projects, :controller => :project do - resources :companies do - resources :people - resource :avatar, :controller => :avatar - end - end - end - - get '/projects/1/companies' - assert_equal 'companies#index', @response.body - assert_equal '/projects/1/companies', project_companies_path(:project_id => '1') - - get '/projects/1/companies/1/people' - assert_equal 'people#index', @response.body - assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1') - - get '/projects/1/companies/1/avatar' - assert_equal 'avatar#show', @response.body - assert_equal '/projects/1/companies/1/avatar', project_company_avatar_path(:project_id => '1', :company_id => '1') - end - - def test_project_manager - draw do - resources :projects do - resource :manager, :as => :super_manager do - post :fire - end - end - end - - get '/projects/1/manager' - assert_equal 'managers#show', @response.body - assert_equal '/projects/1/manager', project_super_manager_path(:project_id => '1') - - get '/projects/1/manager/new' - assert_equal 'managers#new', @response.body - assert_equal '/projects/1/manager/new', new_project_super_manager_path(:project_id => '1') - - post '/projects/1/manager/fire' - assert_equal 'managers#fire', @response.body - assert_equal '/projects/1/manager/fire', fire_project_super_manager_path(:project_id => '1') - end - - def test_project_images - draw do - resources :projects do - resources :images, :as => :funny_images do - post :revise, :on => :member - end - end - end - - get '/projects/1/images' - assert_equal 'images#index', @response.body - assert_equal '/projects/1/images', project_funny_images_path(:project_id => '1') - - get '/projects/1/images/new' - assert_equal 'images#new', @response.body - assert_equal '/projects/1/images/new', new_project_funny_image_path(:project_id => '1') - - post '/projects/1/images/1/revise' - assert_equal 'images#revise', @response.body - assert_equal '/projects/1/images/1/revise', revise_project_funny_image_path(:project_id => '1', :id => '1') - end - - def test_projects_people - draw do - resources :projects do - resources :people do - nested do - scope "/:access_token" do - resource :avatar - end - end - - member do - put :accessible_projects - post :resend, :generate_new_password - end - end - end - end - - get '/projects/1/people' - assert_equal 'people#index', @response.body - assert_equal '/projects/1/people', project_people_path(:project_id => '1') - - get '/projects/1/people/1' - assert_equal 'people#show', @response.body - assert_equal '/projects/1/people/1', project_person_path(:project_id => '1', :id => '1') - - get '/projects/1/people/1/7a2dec8/avatar' - assert_equal 'avatars#show', @response.body - assert_equal '/projects/1/people/1/7a2dec8/avatar', project_person_avatar_path(:project_id => '1', :person_id => '1', :access_token => '7a2dec8') - - put '/projects/1/people/1/accessible_projects' - assert_equal 'people#accessible_projects', @response.body - assert_equal '/projects/1/people/1/accessible_projects', accessible_projects_project_person_path(:project_id => '1', :id => '1') - - post '/projects/1/people/1/resend' - assert_equal 'people#resend', @response.body - assert_equal '/projects/1/people/1/resend', resend_project_person_path(:project_id => '1', :id => '1') - - post '/projects/1/people/1/generate_new_password' - assert_equal 'people#generate_new_password', @response.body - assert_equal '/projects/1/people/1/generate_new_password', generate_new_password_project_person_path(:project_id => '1', :id => '1') - end - - def test_projects_with_resources_path_names - draw do - resources_path_names :correlation_indexes => "info_about_correlation_indexes" - - resources :projects do - get :correlation_indexes, :on => :collection - end - end - - get '/projects/info_about_correlation_indexes' - assert_equal 'projects#correlation_indexes', @response.body - assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path - end - - def test_projects_posts - draw do - resources :projects do - 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 - end - - get '/projects/1/posts' - assert_equal 'posts#index', @response.body - assert_equal '/projects/1/posts', project_posts_path(:project_id => '1') - - get '/projects/1/posts/archive' - assert_equal 'posts#archive', @response.body - assert_equal '/projects/1/posts/archive', archive_project_posts_path(:project_id => '1') - - get '/projects/1/posts/toggle_view' - assert_equal 'posts#toggle_view', @response.body - assert_equal '/projects/1/posts/toggle_view', toggle_view_project_posts_path(:project_id => '1') - - post '/projects/1/posts/1/preview' - assert_equal 'posts#preview', @response.body - assert_equal '/projects/1/posts/1/preview', preview_project_post_path(:project_id => '1', :id => '1') - - get '/projects/1/posts/1/subscription' - assert_equal 'subscriptions#show', @response.body - assert_equal '/projects/1/posts/1/subscription', project_post_subscription_path(:project_id => '1', :post_id => '1') - - get '/projects/1/posts/1/comments' - assert_equal 'comments#index', @response.body - assert_equal '/projects/1/posts/1/comments', project_post_comments_path(:project_id => '1', :post_id => '1') - - post '/projects/1/posts/1/comments/preview' - assert_equal 'comments#preview', @response.body - assert_equal '/projects/1/posts/1/comments/preview', preview_project_post_comments_path(:project_id => '1', :post_id => '1') - end - - def test_replies - draw do - resources :replies do - member do - put :answer, :action => :mark_as_answer - delete :answer, :action => :unmark_as_answer - end - end - end - - put '/replies/1/answer' - assert_equal 'replies#mark_as_answer', @response.body - - delete '/replies/1/answer' - assert_equal 'replies#unmark_as_answer', @response.body - end - - def test_resource_routes_with_only_and_except - draw do - resources :posts, :only => [:index, :show] do - resources :comments, :except => :destroy - end - end - - get '/posts' - assert_equal 'posts#index', @response.body - assert_equal '/posts', posts_path - - get '/posts/1' - assert_equal 'posts#show', @response.body - assert_equal '/posts/1', post_path(:id => 1) - - get '/posts/1/comments' - assert_equal 'comments#index', @response.body - assert_equal '/posts/1/comments', post_comments_path(:post_id => 1) - - post '/posts' - assert_equal 'pass', @response.headers['X-Cascade'] - put '/posts/1' - assert_equal 'pass', @response.headers['X-Cascade'] - delete '/posts/1' - assert_equal 'pass', @response.headers['X-Cascade'] - delete '/posts/1/comments' - assert_equal 'pass', @response.headers['X-Cascade'] - end - - def test_resource_routes_only_create_update_destroy - draw do - resource :past, :only => :destroy - resource :present, :only => :update - resource :future, :only => :create - end - - delete '/past' - assert_equal 'pasts#destroy', @response.body - assert_equal '/past', past_path - - patch '/present' - assert_equal 'presents#update', @response.body - assert_equal '/present', present_path - - put '/present' - assert_equal 'presents#update', @response.body - assert_equal '/present', present_path - - post '/future' - assert_equal 'futures#create', @response.body - assert_equal '/future', future_path - end - - def test_resources_routes_only_create_update_destroy - draw do - resources :relationships, :only => [:create, :destroy] - resources :friendships, :only => [:update] - end - - post '/relationships' - assert_equal 'relationships#create', @response.body - assert_equal '/relationships', relationships_path - - delete '/relationships/1' - assert_equal 'relationships#destroy', @response.body - assert_equal '/relationships/1', relationship_path(1) - - patch '/friendships/1' - assert_equal 'friendships#update', @response.body - assert_equal '/friendships/1', friendship_path(1) - - put '/friendships/1' - assert_equal 'friendships#update', @response.body - assert_equal '/friendships/1', friendship_path(1) - end - - def test_resource_with_slugs_in_ids - draw do - resources :posts - end - - get '/posts/rails-rocks' - assert_equal 'posts#show', @response.body - assert_equal '/posts/rails-rocks', post_path(:id => 'rails-rocks') - end - - def test_resources_for_uncountable_names - draw do - resources :sheep do - get "_it", :on => :member - end - end - - assert_equal '/sheep', sheep_index_path - assert_equal '/sheep/1', sheep_path(1) - assert_equal '/sheep/new', new_sheep_path - assert_equal '/sheep/1/edit', edit_sheep_path(1) - assert_equal '/sheep/1/_it', _it_sheep_path(1) - end - - def test_resource_does_not_modify_passed_options - options = {:id => /.+?/, :format => /json|xml/} - draw { resource :user, options } - assert_equal({:id => /.+?/, :format => /json|xml/}, options) - end - - def test_resources_does_not_modify_passed_options - options = {:id => /.+?/, :format => /json|xml/} - draw { resources :users, options } - assert_equal({:id => /.+?/, :format => /json|xml/}, options) - end - - def test_path_names - draw do - scope 'pt', :as => 'pt' do - resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' - resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do - put :activate, :on => :member - end - end - end - - get '/pt/projetos' - assert_equal 'projects#index', @response.body - assert_equal '/pt/projetos', pt_projects_path - - get '/pt/projetos/1/editar' - assert_equal 'projects#edit', @response.body - assert_equal '/pt/projetos/1/editar', edit_pt_project_path(1) - - get '/pt/administrador' - assert_equal 'admins#show', @response.body - assert_equal '/pt/administrador', pt_admin_path - - get '/pt/administrador/novo' - assert_equal 'admins#new', @response.body - assert_equal '/pt/administrador/novo', new_pt_admin_path - - put '/pt/administrador/ativar' - assert_equal 'admins#activate', @response.body - assert_equal '/pt/administrador/ativar', activate_pt_admin_path - end - - def test_path_option_override - draw do - scope 'pt', :as => 'pt' do - resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do - put :close, :on => :member, :path => 'fechar' - get :open, :on => :new, :path => 'abrir' - end - end - end - - get '/pt/projetos/novo/abrir' - assert_equal 'projects#open', @response.body - assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path - - put '/pt/projetos/1/fechar' - assert_equal 'projects#close', @response.body - assert_equal '/pt/projetos/1/fechar', close_pt_project_path(1) - end - - def test_sprockets - draw do - get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp - end - - get '/sprockets.js' - assert_equal 'javascripts', @response.body - end - - def test_update_person_route - draw do - get 'people/:id/update', :to => 'people#update', :as => :update_person - end - - get '/people/1/update' - assert_equal 'people#update', @response.body - - assert_equal '/people/1/update', update_person_path(:id => 1) - end - - def test_update_project_person - draw do - get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person - end - - get '/projects/1/people/2/update' - assert_equal 'people#update', @response.body - - assert_equal '/projects/1/people/2/update', update_project_person_path(:project_id => 1, :id => 2) - end - - def test_forum_products - draw do - namespace :forum do - resources :products, :path => '' do - resources :questions - end - end - end - - get '/forum' - assert_equal 'forum/products#index', @response.body - assert_equal '/forum', forum_products_path - - get '/forum/basecamp' - assert_equal 'forum/products#show', @response.body - assert_equal '/forum/basecamp', forum_product_path(:id => 'basecamp') - - get '/forum/basecamp/questions' - assert_equal 'forum/questions#index', @response.body - assert_equal '/forum/basecamp/questions', forum_product_questions_path(:product_id => 'basecamp') - - get '/forum/basecamp/questions/1' - assert_equal 'forum/questions#show', @response.body - assert_equal '/forum/basecamp/questions/1', forum_product_question_path(:product_id => 'basecamp', :id => 1) - end - - def test_articles_perma - draw do - get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article - end - - get '/articles/2009/08/18/rails-3' - assert_equal 'articles#show', @response.body - - assert_equal '/articles/2009/8/18/rails-3', article_path(:year => 2009, :month => 8, :day => 18, :title => 'rails-3') - end - - def test_account_namespace - draw do - namespace :account do - resource :subscription, :credit, :credit_card - end - end - - get '/account/subscription' - assert_equal 'account/subscriptions#show', @response.body - assert_equal '/account/subscription', account_subscription_path - - get '/account/credit' - assert_equal 'account/credits#show', @response.body - assert_equal '/account/credit', account_credit_path - - get '/account/credit_card' - assert_equal 'account/credit_cards#show', @response.body - assert_equal '/account/credit_card', account_credit_card_path - end - - def test_nested_namespace - draw do - namespace :account do - namespace :admin do - resource :subscription - end - end - end - - get '/account/admin/subscription' - assert_equal 'account/admin/subscriptions#show', @response.body - assert_equal '/account/admin/subscription', account_admin_subscription_path - end - - def test_namespace_nested_in_resources - draw do - resources :clients do - namespace :google do - resource :account do - namespace :secret do - resource :info - end - end - end - end - end - - get '/clients/1/google/account' - assert_equal '/clients/1/google/account', client_google_account_path(1) - assert_equal 'google/accounts#show', @response.body - - get '/clients/1/google/account/secret/info' - assert_equal '/clients/1/google/account/secret/info', client_google_account_secret_info_path(1) - assert_equal 'google/secret/infos#show', @response.body - end - - def test_namespace_with_options - draw do - namespace :users, :path => 'usuarios' do - root :to => 'home#index' - end - end - - get '/usuarios' - assert_equal '/usuarios', users_root_path - assert_equal 'users/home#index', @response.body - end - - def test_namespaced_shallow_routes_with_module_option - draw do - namespace :foo, module: 'bar' do - resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :show], shallow: true - end - end - end - - get '/foo/posts' - assert_equal '/foo/posts', foo_posts_path - assert_equal 'bar/posts#index', @response.body - - get '/foo/posts/1' - assert_equal '/foo/posts/1', foo_post_path('1') - assert_equal 'bar/posts#show', @response.body - - get '/foo/posts/1/comments' - assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') - assert_equal 'bar/comments#index', @response.body - - get '/foo/comments/2' - assert_equal '/foo/comments/2', foo_comment_path('2') - assert_equal 'bar/comments#show', @response.body - end - - def test_namespaced_shallow_routes_with_path_option - draw do - namespace :foo, path: 'bar' do - resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :show], shallow: true - end - end - end - - get '/bar/posts' - assert_equal '/bar/posts', foo_posts_path - assert_equal 'foo/posts#index', @response.body - - get '/bar/posts/1' - assert_equal '/bar/posts/1', foo_post_path('1') - assert_equal 'foo/posts#show', @response.body - - get '/bar/posts/1/comments' - assert_equal '/bar/posts/1/comments', foo_post_comments_path('1') - assert_equal 'foo/comments#index', @response.body - - get '/bar/comments/2' - assert_equal '/bar/comments/2', foo_comment_path('2') - assert_equal 'foo/comments#show', @response.body - end - - def test_namespaced_shallow_routes_with_as_option - draw do - namespace :foo, as: 'bar' do - resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :show], shallow: true - end - end - end - - get '/foo/posts' - assert_equal '/foo/posts', bar_posts_path - assert_equal 'foo/posts#index', @response.body - - get '/foo/posts/1' - assert_equal '/foo/posts/1', bar_post_path('1') - assert_equal 'foo/posts#show', @response.body - - get '/foo/posts/1/comments' - assert_equal '/foo/posts/1/comments', bar_post_comments_path('1') - assert_equal 'foo/comments#index', @response.body - - get '/foo/comments/2' - assert_equal '/foo/comments/2', bar_comment_path('2') - assert_equal 'foo/comments#show', @response.body - end - - def test_namespaced_shallow_routes_with_shallow_path_option - draw do - namespace :foo, shallow_path: 'bar' do - resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :show], shallow: true - end - end - end - - get '/foo/posts' - assert_equal '/foo/posts', foo_posts_path - assert_equal 'foo/posts#index', @response.body - - get '/foo/posts/1' - assert_equal '/foo/posts/1', foo_post_path('1') - assert_equal 'foo/posts#show', @response.body - - get '/foo/posts/1/comments' - assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') - assert_equal 'foo/comments#index', @response.body - - get '/bar/comments/2' - assert_equal '/bar/comments/2', foo_comment_path('2') - assert_equal 'foo/comments#show', @response.body - end - - def test_namespaced_shallow_routes_with_shallow_prefix_option - draw do - namespace :foo, shallow_prefix: 'bar' do - resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :show], shallow: true - end - end - end - - get '/foo/posts' - assert_equal '/foo/posts', foo_posts_path - assert_equal 'foo/posts#index', @response.body - - get '/foo/posts/1' - assert_equal '/foo/posts/1', foo_post_path('1') - assert_equal 'foo/posts#show', @response.body - - get '/foo/posts/1/comments' - assert_equal '/foo/posts/1/comments', foo_post_comments_path('1') - assert_equal 'foo/comments#index', @response.body - - get '/foo/comments/2' - assert_equal '/foo/comments/2', bar_comment_path('2') - assert_equal 'foo/comments#show', @response.body - end - - def test_namespace_containing_numbers - draw do - namespace :v2 do - resources :subscriptions - end - end - - get '/v2/subscriptions' - assert_equal 'v2/subscriptions#index', @response.body - assert_equal '/v2/subscriptions', v2_subscriptions_path - end - - def test_articles_with_id - draw do - controller :articles do - scope '/articles', :as => 'article' do - scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do - get '/:id', :action => :with_id, :as => "" - end - end - end - end - - get '/articles/rails/1' - assert_equal 'articles#with_id', @response.body - - get '/articles/123/1' - assert_equal 'pass', @response.headers['X-Cascade'] - - assert_equal '/articles/rails/1', article_with_title_path(:title => 'rails', :id => 1) - end - - def test_access_token_rooms - draw do - scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do - resources :rooms - end - end - - 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 - - def test_root - draw do - root :to => 'projects#index' - end - - assert_equal '/', root_path - get '/' - assert_equal 'projects#index', @response.body - end - - def test_scoped_root - draw do - scope '(:locale)', :locale => /en|pl/ do - root :to => 'projects#index' - end - end - - assert_equal '/en', root_path(:locale => 'en') - get '/en' - assert_equal 'projects#index', @response.body - end - - def test_scoped_root_as_name - draw do - scope '(:locale)', :locale => /en|pl/ do - root :to => 'projects#index', :as => 'projects' - end - end - - assert_equal '/en', projects_path(:locale => 'en') - assert_equal '/', projects_path - get '/en' - assert_equal 'projects#index', @response.body - end - - def test_scope_with_format_option - draw do - get "direct/index", as: :no_format_direct, format: false - - scope format: false do - get "scoped/index", as: :no_format_scoped - end - end - - assert_equal "/direct/index", no_format_direct_path - assert_equal "/direct/index?format=html", no_format_direct_path(format: "html") - - assert_equal "/scoped/index", no_format_scoped_path - assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html") - - get '/scoped/index' - assert_equal "scoped#index", @response.body - - get '/scoped/index.html' - assert_equal "Not Found", @response.body - end - - def test_resources_with_format_false_from_scope - draw do - scope format: false do - resources :posts - resource :user - end - end - - get "/posts" - assert_response :success - assert_equal "posts#index", @response.body - assert_equal "/posts", posts_path - - get "/posts.html" - assert_response :not_found - assert_equal "Not Found", @response.body - assert_equal "/posts?format=html", posts_path(format: "html") - - get "/user" - assert_response :success - assert_equal "users#show", @response.body - assert_equal "/user", user_path - - get "/user.html" - assert_response :not_found - assert_equal "Not Found", @response.body - assert_equal "/user?format=html", user_path(format: "html") - end - - def test_index - draw do - get '/info' => 'projects#info', :as => 'info' - end - - assert_equal '/info', info_path - get '/info' - assert_equal 'projects#info', @response.body - end - - def test_match_with_many_paths_containing_a_slash - draw do - get 'get/first', 'get/second', 'get/third', :to => 'get#show' - end - - get '/get/first' - assert_equal 'get#show', @response.body - - get '/get/second' - assert_equal 'get#show', @response.body - - get '/get/third' - assert_equal 'get#show', @response.body - end - - def test_match_shorthand_with_no_scope - draw do - get 'account/overview' - end - - assert_equal '/account/overview', account_overview_path - get '/account/overview' - assert_equal 'account#overview', @response.body - end - - def test_match_shorthand_inside_namespace - draw do - namespace :account do - get 'shorthand' - end - end - - assert_equal '/account/shorthand', account_shorthand_path - get '/account/shorthand' - assert_equal 'account#shorthand', @response.body - end - - def test_match_shorthand_with_multiple_paths_inside_namespace - draw do - namespace :proposals do - put 'activate', 'inactivate' - end - end - - put '/proposals/activate' - assert_equal 'proposals#activate', @response.body - - put '/proposals/inactivate' - assert_equal 'proposals#inactivate', @response.body - end - - def test_match_shorthand_inside_namespace_with_controller - draw do - namespace :api do - get "products/list" - end - end - - assert_equal '/api/products/list', api_products_list_path - get '/api/products/list' - assert_equal 'api/products#list', @response.body - end - - def test_match_shorthand_inside_scope_with_variables_with_controller - draw do - scope ':locale' do - match 'questions/new', via: [:get] - end - end - - get '/de/questions/new' - assert_equal 'questions#new', @response.body - assert_equal 'de', @request.params[:locale] - end - - def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller - draw do - namespace :api do - namespace :v3 do - scope ':locale' do - get "products/list" - end - end - end - end - - get '/api/v3/en/products/list' - assert_equal 'api/v3/products#list', @response.body - end - - def test_controller_option_with_nesting_and_leading_slash - draw do - scope '/job', controller: 'job' do - scope ':id', action: 'manage_applicant' do - get "/active" - end - end - end - - get '/job/5/active' - assert_equal 'job#manage_applicant', @response.body - end - - def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper - draw do - resources :replies do - collection do - get 'page/:page' => 'replies#index', :page => %r{\d+} - get ':page' => 'replies#index', :page => %r{\d+} - end - end - end - - assert_equal '/replies', replies_path - end - - def test_scoped_controller_with_namespace_and_action - draw do - namespace :account do - get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback - end - end - - assert_equal '/account/twitter/callback', account_callback_path("twitter") - get '/account/twitter/callback' - assert_equal 'account/callbacks#twitter', @response.body - - get '/account/whatever/callback' - assert_equal 'Not Found', @response.body - end - - def test_convention_match_nested_and_with_leading_slash - draw do - get '/account/nested/overview' - end - - assert_equal '/account/nested/overview', account_nested_overview_path - get '/account/nested/overview' - assert_equal 'account/nested#overview', @response.body - end - - def test_convention_with_explicit_end - draw do - get 'sign_in' => "sessions#new" - end - - get '/sign_in' - assert_equal 'sessions#new', @response.body - assert_equal '/sign_in', sign_in_path - end - - def test_redirect_with_complete_url_and_status - draw do - get 'account/google' => redirect('http://www.google.com/', :status => 302) - end - - get '/account/google' - verify_redirect 'http://www.google.com/', 302 - end - - def test_redirect_with_port - draw do - get 'account/login', :to => redirect("/login") - end - - previous_host, self.host = self.host, 'www.example.com:3000' - - get '/account/login' - verify_redirect 'http://www.example.com:3000/login' - ensure - self.host = previous_host - end - - def test_normalize_namespaced_matches - draw do - namespace :account do - get 'description', :action => :description, :as => "description" - end - end - - assert_equal '/account/description', account_description_path - - get '/account/description' - assert_equal 'account#description', @response.body - end - - def test_namespaced_roots - draw do - namespace :account do - root :to => "account#index" - end - end - - assert_equal '/account', account_root_path - get '/account' - assert_equal 'account/account#index', @response.body - end - - def test_optional_scoped_root - draw do - scope '(:locale)', :locale => /en|pl/ do - root :to => 'projects#index' - end - end - - assert_equal '/en', root_path("en") - get '/en' - assert_equal 'projects#index', @response.body - end - - def test_optional_scoped_path - draw do - scope '(:locale)', :locale => /en|pl/ do - resources :descriptions - end - end - - assert_equal '/en/descriptions', descriptions_path("en") - assert_equal '/descriptions', descriptions_path(nil) - assert_equal '/en/descriptions/1', description_path("en", 1) - assert_equal '/descriptions/1', description_path(nil, 1) - - get '/en/descriptions' - assert_equal 'descriptions#index', @response.body - - get '/descriptions' - assert_equal 'descriptions#index', @response.body - - get '/en/descriptions/1' - assert_equal 'descriptions#show', @response.body - - get '/descriptions/1' - assert_equal 'descriptions#show', @response.body - end - - def test_nested_optional_scoped_path - draw do - namespace :admin do - scope '(:locale)', :locale => /en|pl/ do - resources :descriptions - end - end - end - - assert_equal '/admin/en/descriptions', admin_descriptions_path("en") - assert_equal '/admin/descriptions', admin_descriptions_path(nil) - assert_equal '/admin/en/descriptions/1', admin_description_path("en", 1) - assert_equal '/admin/descriptions/1', admin_description_path(nil, 1) - - get '/admin/en/descriptions' - assert_equal 'admin/descriptions#index', @response.body - - get '/admin/descriptions' - assert_equal 'admin/descriptions#index', @response.body - - get '/admin/en/descriptions/1' - assert_equal 'admin/descriptions#show', @response.body - - get '/admin/descriptions/1' - assert_equal 'admin/descriptions#show', @response.body - end - - def test_nested_optional_path_shorthand - draw do - scope '(:locale)', :locale => /en|pl/ do - get "registrations/new" - end - end - - get '/registrations/new' - assert_nil @request.params[:locale] - - get '/en/registrations/new' - assert_equal 'en', @request.params[:locale] - end - - def test_default_string_params - draw do - get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home' - get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' } - - defaults :id => 'home' do - get 'scoped_pages/(:id)', :to => 'pages#show' - end - end - - get '/inline_pages' - assert_equal 'home', @request.params[:id] - - get '/default_pages' - assert_equal 'home', @request.params[:id] - - get '/scoped_pages' - assert_equal 'home', @request.params[:id] - end - - def test_default_integer_params - draw do - get 'inline_pages/(:page)', to: 'pages#show', page: 1 - get 'default_pages/(:page)', to: 'pages#show', defaults: { page: 1 } - - defaults page: 1 do - get 'scoped_pages/(:page)', to: 'pages#show' - end - end - - get '/inline_pages' - assert_equal 1, @request.params[:page] - - get '/default_pages' - assert_equal 1, @request.params[:page] - - get '/scoped_pages' - assert_equal 1, @request.params[:page] - end - - def test_resource_constraints - draw do - resources :products, :constraints => { :id => /\d{4}/ } do - root :to => "products#root" - get :favorite, :on => :collection - resources :images - end - - resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } - end - - get '/products/1' - assert_equal 'pass', @response.headers['X-Cascade'] - get '/products' - assert_equal 'products#root', @response.body - get '/products/favorite' - assert_equal 'products#favorite', @response.body - get '/products/0001' - assert_equal 'products#show', @response.body - - get '/products/1/images' - assert_equal 'pass', @response.headers['X-Cascade'] - get '/products/0001/images' - assert_equal 'images#index', @response.body - get '/products/0001/images/0001' - assert_equal 'images#show', @response.body - - get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} - assert_equal 'pass', @response.headers['X-Cascade'] - get '/dashboard', {}, {'REMOTE_ADDR' => '192.168.1.100'} - assert_equal 'dashboards#show', @response.body - end - - def test_root_works_in_the_resources_scope - draw do - resources :products do - root :to => "products#root" - end - end - - get '/products' - assert_equal 'products#root', @response.body - assert_equal '/products', products_root_path - end - - def test_module_scope - draw do - resource :token, :module => :api - end - - get '/token' - assert_equal 'api/tokens#show', @response.body - assert_equal '/token', token_path - end - - def test_path_scope - draw do - scope :path => 'api' do - resource :me - get '/' => 'mes#index' - end - end - - get '/api/me' - assert_equal 'mes#show', @response.body - assert_equal '/api/me', me_path - - get '/api' - assert_equal 'mes#index', @response.body - end - - def test_symbol_scope - draw do - scope :path => 'api' do - scope :v2 do - resource :me, as: 'v2_me' - get '/' => 'mes#index' - end - - scope :v3, :admin do - resource :me, as: 'v3_me' - end - end - end - - get '/api/v2/me' - assert_equal 'mes#show', @response.body - assert_equal '/api/v2/me', v2_me_path - - get '/api/v2' - assert_equal 'mes#index', @response.body - - get '/api/v3/admin/me' - assert_equal 'mes#show', @response.body - end - - def test_url_generator_for_generic_route - draw do - get "whatever/:controller(/:action(/:id))" - end - - get '/whatever/foo/bar' - assert_equal 'foo#bar', @response.body - - assert_equal 'http://www.example.com/whatever/foo/bar/1', - url_for(:controller => "foo", :action => "bar", :id => 1) - end - - def test_url_generator_for_namespaced_generic_route - draw do - get "whatever/:controller(/:action(/:id))", :id => /\d+/ - end - - get '/whatever/foo/bar/show' - assert_equal 'foo/bar#show', @response.body - - get '/whatever/foo/bar/show/1' - assert_equal 'foo/bar#show', @response.body - - assert_equal 'http://www.example.com/whatever/foo/bar/show', - url_for(:controller => "foo/bar", :action => "show") - - assert_equal 'http://www.example.com/whatever/foo/bar/show/1', - url_for(:controller => "foo/bar", :action => "show", :id => '1') - end - - def test_resource_new_actions - draw do - resources :replies do - new do - post :preview - end - end - - scope 'pt', :as => 'pt' do - resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do - post :preview, :on => :new - end - - resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do - post :preview, :on => :new - end - - resources :products, :path_names => { :new => 'novo' } do - new do - post :preview - end - end - end - - resource :profile do - new do - post :preview - end - end - end - - assert_equal '/replies/new/preview', preview_new_reply_path - assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path - assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path - assert_equal '/pt/products/novo/preview', preview_new_pt_product_path - assert_equal '/profile/new/preview', preview_new_profile_path - - post '/replies/new/preview' - assert_equal 'replies#preview', @response.body - - post '/pt/projetos/novo/preview' - assert_equal 'projects#preview', @response.body - - post '/pt/administrador/novo/preview' - assert_equal 'admins#preview', @response.body - - post '/pt/products/novo/preview' - assert_equal 'products#preview', @response.body - - post '/profile/new/preview' - assert_equal 'profiles#preview', @response.body - end - - def test_resource_merges_options_from_scope - draw do - scope :only => :show do - resource :account - end - end - - assert_raise(NoMethodError) { new_account_path } - - get '/account/new' - assert_equal 404, status - end - - def test_resources_merges_options_from_scope - draw do - scope :only => [:index, :show] do - resources :products do - resources :images - end - end - end - - assert_raise(NoMethodError) { edit_product_path('1') } - - get '/products/1/edit' - assert_equal 404, status - - assert_raise(NoMethodError) { edit_product_image_path('1', '2') } - - post '/products/1/images/2/edit' - assert_equal 404, status - end - - def test_shallow_nested_resources - draw do - shallow do - namespace :api do - resources :teams do - resources :players - resource :captain - end - end - end - - resources :threads, :shallow => true do - resource :owner - resources :messages do - resources :comments do - member do - post :preview - end - end - end - end - end - - get '/api/teams' - assert_equal 'api/teams#index', @response.body - assert_equal '/api/teams', api_teams_path - - get '/api/teams/new' - assert_equal 'api/teams#new', @response.body - assert_equal '/api/teams/new', new_api_team_path - - get '/api/teams/1' - assert_equal 'api/teams#show', @response.body - assert_equal '/api/teams/1', api_team_path(:id => '1') - - get '/api/teams/1/edit' - assert_equal 'api/teams#edit', @response.body - assert_equal '/api/teams/1/edit', edit_api_team_path(:id => '1') - - get '/api/teams/1/players' - assert_equal 'api/players#index', @response.body - assert_equal '/api/teams/1/players', api_team_players_path(:team_id => '1') - - get '/api/teams/1/players/new' - assert_equal 'api/players#new', @response.body - assert_equal '/api/teams/1/players/new', new_api_team_player_path(:team_id => '1') - - get '/api/players/2' - assert_equal 'api/players#show', @response.body - assert_equal '/api/players/2', api_player_path(:id => '2') - - get '/api/players/2/edit' - assert_equal 'api/players#edit', @response.body - assert_equal '/api/players/2/edit', edit_api_player_path(:id => '2') - - get '/api/teams/1/captain' - assert_equal 'api/captains#show', @response.body - assert_equal '/api/teams/1/captain', api_team_captain_path(:team_id => '1') - - get '/api/teams/1/captain/new' - assert_equal 'api/captains#new', @response.body - assert_equal '/api/teams/1/captain/new', new_api_team_captain_path(:team_id => '1') - - get '/api/teams/1/captain/edit' - assert_equal 'api/captains#edit', @response.body - assert_equal '/api/teams/1/captain/edit', edit_api_team_captain_path(:team_id => '1') - - get '/threads' - assert_equal 'threads#index', @response.body - assert_equal '/threads', threads_path - - get '/threads/new' - assert_equal 'threads#new', @response.body - assert_equal '/threads/new', new_thread_path - - get '/threads/1' - assert_equal 'threads#show', @response.body - assert_equal '/threads/1', thread_path(:id => '1') - - get '/threads/1/edit' - assert_equal 'threads#edit', @response.body - assert_equal '/threads/1/edit', edit_thread_path(:id => '1') - - get '/threads/1/owner' - assert_equal 'owners#show', @response.body - assert_equal '/threads/1/owner', thread_owner_path(:thread_id => '1') - - get '/threads/1/messages' - assert_equal 'messages#index', @response.body - assert_equal '/threads/1/messages', thread_messages_path(:thread_id => '1') - - get '/threads/1/messages/new' - assert_equal 'messages#new', @response.body - assert_equal '/threads/1/messages/new', new_thread_message_path(:thread_id => '1') - - get '/messages/2' - assert_equal 'messages#show', @response.body - assert_equal '/messages/2', message_path(:id => '2') - - get '/messages/2/edit' - assert_equal 'messages#edit', @response.body - assert_equal '/messages/2/edit', edit_message_path(:id => '2') - - get '/messages/2/comments' - assert_equal 'comments#index', @response.body - assert_equal '/messages/2/comments', message_comments_path(:message_id => '2') - - get '/messages/2/comments/new' - assert_equal 'comments#new', @response.body - assert_equal '/messages/2/comments/new', new_message_comment_path(:message_id => '2') - - get '/comments/3' - assert_equal 'comments#show', @response.body - assert_equal '/comments/3', comment_path(:id => '3') - - get '/comments/3/edit' - assert_equal 'comments#edit', @response.body - assert_equal '/comments/3/edit', edit_comment_path(:id => '3') - - post '/comments/3/preview' - assert_equal 'comments#preview', @response.body - assert_equal '/comments/3/preview', preview_comment_path(:id => '3') - end - - def test_shallow_nested_resources_inside_resource - draw do - resource :membership, shallow: true do - resources :cards - end - end - - get '/membership/cards' - assert_equal 'cards#index', @response.body - assert_equal '/membership/cards', membership_cards_path - - get '/membership/cards/new' - assert_equal 'cards#new', @response.body - assert_equal '/membership/cards/new', new_membership_card_path - - post '/membership/cards' - assert_equal 'cards#create', @response.body - - get '/cards/1' - assert_equal 'cards#show', @response.body - assert_equal '/cards/1', card_path('1') - - get '/cards/1/edit' - assert_equal 'cards#edit', @response.body - assert_equal '/cards/1/edit', edit_card_path('1') - - put '/cards/1' - assert_equal 'cards#update', @response.body - - patch '/cards/1' - assert_equal 'cards#update', @response.body - - delete '/cards/1' - assert_equal 'cards#destroy', @response.body - end - - def test_shallow_deeply_nested_resources - draw do - resources :blogs do - resources :posts do - resources :comments, shallow: true - end - end - end - - get '/comments/1' - assert_equal 'comments#show', @response.body - - assert_equal '/comments/1', comment_path('1') - assert_equal '/blogs/new', new_blog_path - assert_equal '/blogs/1/posts/new', new_blog_post_path(:blog_id => 1) - assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2) - end - - def test_shallow_nested_resources_within_scope - draw do - scope '/hello' do - shallow do - resources :notes do - resources :trackbacks - end - end - end - end - - get '/hello/notes/1/trackbacks' - assert_equal 'trackbacks#index', @response.body - assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) - - get '/hello/notes/1/edit' - assert_equal 'notes#edit', @response.body - assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') - - get '/hello/notes/1/trackbacks/new' - assert_equal 'trackbacks#new', @response.body - assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) - - get '/hello/trackbacks/1' - assert_equal 'trackbacks#show', @response.body - assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') - - get '/hello/trackbacks/1/edit' - assert_equal 'trackbacks#edit', @response.body - assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') - - put '/hello/trackbacks/1' - assert_equal 'trackbacks#update', @response.body - - post '/hello/notes/1/trackbacks' - assert_equal 'trackbacks#create', @response.body - - delete '/hello/trackbacks/1' - assert_equal 'trackbacks#destroy', @response.body - - get '/hello/notes' - assert_equal 'notes#index', @response.body - - post '/hello/notes' - assert_equal 'notes#create', @response.body - - get '/hello/notes/new' - assert_equal 'notes#new', @response.body - assert_equal '/hello/notes/new', new_note_path - - get '/hello/notes/1' - assert_equal 'notes#show', @response.body - assert_equal '/hello/notes/1', note_path(:id => 1) - - put '/hello/notes/1' - assert_equal 'notes#update', @response.body - - delete '/hello/notes/1' - assert_equal 'notes#destroy', @response.body - end - - def test_shallow_option_nested_resources_within_scope - draw do - scope '/hello' do - resources :notes, :shallow => true do - resources :trackbacks - end - end - end - - get '/hello/notes/1/trackbacks' - assert_equal 'trackbacks#index', @response.body - assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) - - get '/hello/notes/1/edit' - assert_equal 'notes#edit', @response.body - assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') - - get '/hello/notes/1/trackbacks/new' - assert_equal 'trackbacks#new', @response.body - assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) - - get '/hello/trackbacks/1' - assert_equal 'trackbacks#show', @response.body - assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') - - get '/hello/trackbacks/1/edit' - assert_equal 'trackbacks#edit', @response.body - assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') - - put '/hello/trackbacks/1' - assert_equal 'trackbacks#update', @response.body - - post '/hello/notes/1/trackbacks' - assert_equal 'trackbacks#create', @response.body - - delete '/hello/trackbacks/1' - assert_equal 'trackbacks#destroy', @response.body - - get '/hello/notes' - assert_equal 'notes#index', @response.body - - post '/hello/notes' - assert_equal 'notes#create', @response.body - - get '/hello/notes/new' - assert_equal 'notes#new', @response.body - assert_equal '/hello/notes/new', new_note_path - - get '/hello/notes/1' - assert_equal 'notes#show', @response.body - assert_equal '/hello/notes/1', note_path(:id => 1) - - put '/hello/notes/1' - assert_equal 'notes#update', @response.body - - delete '/hello/notes/1' - assert_equal 'notes#destroy', @response.body - end - - def test_custom_resource_routes_are_scoped - draw do - resources :customers do - get :recent, :on => :collection - get "profile", :on => :member - get "secret/profile" => "customers#secret", :on => :member - post "preview" => "customers#preview", :as => :another_preview, :on => :new - resource :avatar do - get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member - end - resources :invoices do - get "outstanding" => "invoices#outstanding", :on => :collection - get "overdue", :action => :overdue, :on => :collection - get "print" => "invoices#print", :as => :print, :on => :member - post "preview" => "invoices#preview", :as => :preview, :on => :new - end - resources :notes, :shallow => true do - get "preview" => "notes#preview", :as => :preview, :on => :new - get "print" => "notes#print", :as => :print, :on => :member - end - end - - namespace :api do - resources :customers do - get "recent" => "customers#recent", :as => :recent, :on => :collection - get "profile" => "customers#profile", :as => :profile, :on => :member - post "preview" => "customers#preview", :as => :preview, :on => :new - end - end - end - - assert_equal '/customers/recent', recent_customers_path - assert_equal '/customers/1/profile', profile_customer_path(:id => '1') - assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1') - assert_equal '/customers/new/preview', another_preview_new_customer_path - assert_equal '/customers/1/avatar/thumbnail.jpg', thumbnail_customer_avatar_path(:customer_id => '1', :format => :jpg) - assert_equal '/customers/1/invoices/outstanding', outstanding_customer_invoices_path(:customer_id => '1') - assert_equal '/customers/1/invoices/2/print', print_customer_invoice_path(:customer_id => '1', :id => '2') - assert_equal '/customers/1/invoices/new/preview', preview_new_customer_invoice_path(:customer_id => '1') - assert_equal '/customers/1/notes/new/preview', preview_new_customer_note_path(:customer_id => '1') - assert_equal '/notes/1/print', print_note_path(:id => '1') - assert_equal '/api/customers/recent', recent_api_customers_path - assert_equal '/api/customers/1/profile', profile_api_customer_path(:id => '1') - assert_equal '/api/customers/new/preview', preview_new_api_customer_path - - get '/customers/1/invoices/overdue' - assert_equal 'invoices#overdue', @response.body - - get '/customers/1/secret/profile' - assert_equal 'customers#secret', @response.body - end - - def test_shallow_nested_routes_ignore_module - draw do - scope :module => :api do - resources :errors, :shallow => true do - resources :notices - end - end - end - - get '/errors/1/notices' - assert_equal 'api/notices#index', @response.body - assert_equal '/errors/1/notices', error_notices_path(:error_id => '1') - - get '/notices/1' - assert_equal 'api/notices#show', @response.body - assert_equal '/notices/1', notice_path(:id => '1') - end - - def test_non_greedy_regexp - draw do - namespace :api do - scope(':version', :version => /.+/) do - resources :users, :id => /.+?/, :format => /json|xml/ - end - end - end - - get '/api/1.0/users' - assert_equal 'api/users#index', @response.body - assert_equal '/api/1.0/users', api_users_path(:version => '1.0') - - get '/api/1.0/users.json' - assert_equal 'api/users#index', @response.body - assert_equal true, @request.format.json? - assert_equal '/api/1.0/users.json', api_users_path(:version => '1.0', :format => :json) - - get '/api/1.0/users/first.last' - assert_equal 'api/users#show', @response.body - assert_equal 'first.last', @request.params[:id] - assert_equal '/api/1.0/users/first.last', api_user_path(:version => '1.0', :id => 'first.last') - - get '/api/1.0/users/first.last.xml' - assert_equal 'api/users#show', @response.body - assert_equal 'first.last', @request.params[:id] - assert_equal true, @request.format.xml? - assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml) - end - - def test_match_without_via - assert_raises(ArgumentError) do - draw do - match '/foo/bar', :to => 'files#show' - end - end - end - - def test_match_with_empty_via - assert_raises(ArgumentError) do - draw do - match '/foo/bar', :to => 'files#show', :via => [] - end - end - end - - def test_glob_parameter_accepts_regexp - draw do - get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ - end - - get '/en/path/to/existing/file.html' - assert_equal 200, @response.status - end - - def test_resources_controller_name_is_not_pluralized - draw do - resources :content - end - - get '/content' - assert_equal 'content#index', @response.body - end - - def test_url_generator_for_optional_prefix_dynamic_segment - draw do - get "(/:username)/followers" => "followers#index" - end - - get '/bob/followers' - assert_equal 'followers#index', @response.body - assert_equal 'http://www.example.com/bob/followers', - url_for(:controller => "followers", :action => "index", :username => "bob") - - get '/followers' - assert_equal 'followers#index', @response.body - assert_equal 'http://www.example.com/followers', - url_for(:controller => "followers", :action => "index", :username => nil) - end - - def test_url_generator_for_optional_suffix_static_and_dynamic_segment - draw do - get "/groups(/user/:username)" => "groups#index" - end - - get '/groups/user/bob' - assert_equal 'groups#index', @response.body - assert_equal 'http://www.example.com/groups/user/bob', - url_for(:controller => "groups", :action => "index", :username => "bob") - - get '/groups' - assert_equal 'groups#index', @response.body - assert_equal 'http://www.example.com/groups', - url_for(:controller => "groups", :action => "index", :username => nil) - end - - def test_url_generator_for_optional_prefix_static_and_dynamic_segment - draw do - get "(/user/:username)/photos" => "photos#index" - end - - get '/user/bob/photos' - assert_equal 'photos#index', @response.body - assert_equal 'http://www.example.com/user/bob/photos', - url_for(:controller => "photos", :action => "index", :username => "bob") - - get '/photos' - assert_equal 'photos#index', @response.body - assert_equal 'http://www.example.com/photos', - url_for(:controller => "photos", :action => "index", :username => nil) - end - - def test_url_recognition_for_optional_static_segments - draw do - scope '(groups)' do - scope '(discussions)' do - resources :messages - end - end - end - - get '/groups/discussions/messages' - assert_equal 'messages#index', @response.body - - get '/groups/discussions/messages/1' - assert_equal 'messages#show', @response.body - - get '/groups/messages' - assert_equal 'messages#index', @response.body - - get '/groups/messages/1' - assert_equal 'messages#show', @response.body - - get '/discussions/messages' - assert_equal 'messages#index', @response.body - - get '/discussions/messages/1' - assert_equal 'messages#show', @response.body - - get '/messages' - assert_equal 'messages#index', @response.body - - get '/messages/1' - assert_equal 'messages#show', @response.body - end - - def test_router_removes_invalid_conditions - draw do - scope :constraints => { :id => /\d+/ } do - get '/tickets', :to => 'tickets#index', :as => :tickets - end - end - - get '/tickets' - assert_equal 'tickets#index', @response.body - assert_equal '/tickets', tickets_path - end - - def test_constraints_are_merged_from_scope - draw do - scope :constraints => { :id => /\d{4}/ } do - resources :movies do - resources :reviews - resource :trailer - end - end - end - - get '/movies/0001' - assert_equal 'movies#show', @response.body - assert_equal '/movies/0001', movie_path(:id => '0001') - - get '/movies/00001' - assert_equal 'Not Found', @response.body - assert_raises(ActionController::UrlGenerationError){ movie_path(:id => '00001') } - - get '/movies/0001/reviews' - assert_equal 'reviews#index', @response.body - assert_equal '/movies/0001/reviews', movie_reviews_path(:movie_id => '0001') - - get '/movies/00001/reviews' - assert_equal 'Not Found', @response.body - assert_raises(ActionController::UrlGenerationError){ movie_reviews_path(:movie_id => '00001') } - - get '/movies/0001/reviews/0001' - assert_equal 'reviews#show', @response.body - assert_equal '/movies/0001/reviews/0001', movie_review_path(:movie_id => '0001', :id => '0001') - - get '/movies/00001/reviews/0001' - assert_equal 'Not Found', @response.body - assert_raises(ActionController::UrlGenerationError){ movie_path(:movie_id => '00001', :id => '00001') } - - get '/movies/0001/trailer' - assert_equal 'trailers#show', @response.body - assert_equal '/movies/0001/trailer', movie_trailer_path(:movie_id => '0001') - - get '/movies/00001/trailer' - assert_equal 'Not Found', @response.body - assert_raises(ActionController::UrlGenerationError){ movie_trailer_path(:movie_id => '00001') } - end - - def test_only_should_be_read_from_scope - draw do - scope :only => [:index, :show] do - namespace :only do - resources :clubs do - resources :players - resource :chairman - end - end - end - end - - get '/only/clubs' - assert_equal 'only/clubs#index', @response.body - assert_equal '/only/clubs', only_clubs_path - - get '/only/clubs/1/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_only_club_path(:id => '1') } - - get '/only/clubs/1/players' - assert_equal 'only/players#index', @response.body - assert_equal '/only/clubs/1/players', only_club_players_path(:club_id => '1') - - get '/only/clubs/1/players/2/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_only_club_player_path(:club_id => '1', :id => '2') } - - get '/only/clubs/1/chairman' - assert_equal 'only/chairmen#show', @response.body - assert_equal '/only/clubs/1/chairman', only_club_chairman_path(:club_id => '1') - - get '/only/clubs/1/chairman/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_only_club_chairman_path(:club_id => '1') } - end - - def test_except_should_be_read_from_scope - draw do - scope :except => [:new, :create, :edit, :update, :destroy] do - namespace :except do - resources :clubs do - resources :players - resource :chairman - end - end - end - end - - get '/except/clubs' - assert_equal 'except/clubs#index', @response.body - assert_equal '/except/clubs', except_clubs_path - - get '/except/clubs/1/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_except_club_path(:id => '1') } - - get '/except/clubs/1/players' - assert_equal 'except/players#index', @response.body - assert_equal '/except/clubs/1/players', except_club_players_path(:club_id => '1') - - get '/except/clubs/1/players/2/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_except_club_player_path(:club_id => '1', :id => '2') } - - get '/except/clubs/1/chairman' - assert_equal 'except/chairmen#show', @response.body - assert_equal '/except/clubs/1/chairman', except_club_chairman_path(:club_id => '1') - - get '/except/clubs/1/chairman/edit' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_except_club_chairman_path(:club_id => '1') } - end - - def test_only_option_should_override_scope - draw do - scope :only => :show do - namespace :only do - resources :sectors, :only => :index - end - end - end - - get '/only/sectors' - assert_equal 'only/sectors#index', @response.body - assert_equal '/only/sectors', only_sectors_path - - get '/only/sectors/1' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { only_sector_path(:id => '1') } - end - - def test_only_option_should_not_inherit - draw do - scope :only => :show do - namespace :only do - resources :sectors, :only => :index do - resources :companies - resource :leader - end - end - end - end - - get '/only/sectors/1/companies/2' - assert_equal 'only/companies#show', @response.body - assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2') - - get '/only/sectors/1/leader' - assert_equal 'only/leaders#show', @response.body - assert_equal '/only/sectors/1/leader', only_sector_leader_path(:sector_id => '1') - end - - def test_except_option_should_override_scope - draw do - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] - end - end - end - - get '/except/sectors' - assert_equal 'except/sectors#index', @response.body - assert_equal '/except/sectors', except_sectors_path - - get '/except/sectors/1' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { except_sector_path(:id => '1') } - end - - def test_except_option_should_not_inherit - draw do - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] do - resources :companies - resource :leader - end - end - end - end - - get '/except/sectors/1/companies/2' - assert_equal 'except/companies#show', @response.body - assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2') - - get '/except/sectors/1/leader' - assert_equal 'except/leaders#show', @response.body - assert_equal '/except/sectors/1/leader', except_sector_leader_path(:sector_id => '1') - end - - def test_except_option_should_override_scoped_only - draw do - scope :only => :show do - namespace :only do - resources :sectors, :only => :index do - resources :managers, :except => [:show, :update, :destroy] - end - end - end - end - - get '/only/sectors/1/managers' - assert_equal 'only/managers#index', @response.body - assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1') - - get '/only/sectors/1/managers/2' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { only_sector_manager_path(:sector_id => '1', :id => '2') } - end - - def test_only_option_should_override_scoped_except - draw do - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] do - resources :managers, :only => :index - end - end - end - end - - get '/except/sectors/1/managers' - assert_equal 'except/managers#index', @response.body - assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1') - - get '/except/sectors/1/managers/2' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { except_sector_manager_path(:sector_id => '1', :id => '2') } - end - - def test_only_scope_should_override_parent_scope - draw do - scope :only => :show do - namespace :only do - resources :sectors, :only => :index do - resources :companies do - scope :only => :index do - resources :divisions - end - end - end - end - end - end - - get '/only/sectors/1/companies/2/divisions' - assert_equal 'only/divisions#index', @response.body - assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2') - - get '/only/sectors/1/companies/2/divisions/3' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { only_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } - end - - def test_except_scope_should_override_parent_scope - draw do - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] do - resources :companies do - scope :except => [:show, :update, :destroy] do - resources :divisions - end - end - end - end - end - end - - get '/except/sectors/1/companies/2/divisions' - assert_equal 'except/divisions#index', @response.body - assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2') - - get '/except/sectors/1/companies/2/divisions/3' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { except_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } - end - - def test_except_scope_should_override_parent_only_scope - draw do - scope :only => :show do - namespace :only do - resources :sectors, :only => :index do - resources :companies do - scope :except => [:show, :update, :destroy] do - resources :departments - end - end - end - end - end - end - - get '/only/sectors/1/companies/2/departments' - assert_equal 'only/departments#index', @response.body - assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2') - - get '/only/sectors/1/companies/2/departments/3' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { only_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } - end - - def test_only_scope_should_override_parent_except_scope - draw do - scope :except => :index do - namespace :except do - resources :sectors, :except => [:show, :update, :destroy] do - resources :companies do - scope :only => :index do - resources :departments - end - end - end - end - end - end - - get '/except/sectors/1/companies/2/departments' - assert_equal 'except/departments#index', @response.body - assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2') - - get '/except/sectors/1/companies/2/departments/3' - assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { except_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } - end - - def test_resources_are_not_pluralized - draw do - namespace :transport do - resources :taxis - end - end - - get '/transport/taxis' - assert_equal 'transport/taxis#index', @response.body - assert_equal '/transport/taxis', transport_taxis_path - - get '/transport/taxis/new' - assert_equal 'transport/taxis#new', @response.body - assert_equal '/transport/taxis/new', new_transport_taxi_path - - post '/transport/taxis' - assert_equal 'transport/taxis#create', @response.body - - get '/transport/taxis/1' - assert_equal 'transport/taxis#show', @response.body - assert_equal '/transport/taxis/1', transport_taxi_path(:id => '1') - - get '/transport/taxis/1/edit' - assert_equal 'transport/taxis#edit', @response.body - assert_equal '/transport/taxis/1/edit', edit_transport_taxi_path(:id => '1') - - put '/transport/taxis/1' - assert_equal 'transport/taxis#update', @response.body - - delete '/transport/taxis/1' - assert_equal 'transport/taxis#destroy', @response.body - end - - def test_singleton_resources_are_not_singularized - draw do - namespace :medical do - resource :taxis - end - end - - get '/medical/taxis/new' - assert_equal 'medical/taxis#new', @response.body - assert_equal '/medical/taxis/new', new_medical_taxis_path - - post '/medical/taxis' - assert_equal 'medical/taxis#create', @response.body - - get '/medical/taxis' - assert_equal 'medical/taxis#show', @response.body - assert_equal '/medical/taxis', medical_taxis_path - - get '/medical/taxis/edit' - assert_equal 'medical/taxis#edit', @response.body - assert_equal '/medical/taxis/edit', edit_medical_taxis_path - - put '/medical/taxis' - assert_equal 'medical/taxis#update', @response.body - - delete '/medical/taxis' - assert_equal 'medical/taxis#destroy', @response.body - end - - def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action - draw do - resources :sections, :id => /.+/ do - get :preview, :on => :member - end - end - - get '/sections/1/edit' - assert_equal 'sections#edit', @response.body - assert_equal '/sections/1/edit', edit_section_path(:id => '1') - - get '/sections/1/preview' - assert_equal 'sections#preview', @response.body - assert_equal '/sections/1/preview', preview_section_path(:id => '1') - end - - def test_resource_constraints_are_pushed_to_scope - draw do - namespace :wiki do - resources :articles, :id => /[^\/]+/ do - resources :comments, :only => [:create, :new] - end - end - end - - get '/wiki/articles/Ruby_on_Rails_3.0' - assert_equal 'wiki/articles#show', @response.body - assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0') - - get '/wiki/articles/Ruby_on_Rails_3.0/comments/new' - assert_equal 'wiki/comments#new', @response.body - assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments/new', new_wiki_article_comment_path(:article_id => 'Ruby_on_Rails_3.0') - - post '/wiki/articles/Ruby_on_Rails_3.0/comments' - assert_equal 'wiki/comments#create', @response.body - assert_equal '/wiki/articles/Ruby_on_Rails_3.0/comments', wiki_article_comments_path(:article_id => 'Ruby_on_Rails_3.0') - end - - def test_resources_path_can_be_a_symbol - draw do - resources :wiki_pages, :path => :pages - resource :wiki_account, :path => :my_account - end - - get '/pages' - assert_equal 'wiki_pages#index', @response.body - assert_equal '/pages', wiki_pages_path - - get '/pages/Ruby_on_Rails' - assert_equal 'wiki_pages#show', @response.body - assert_equal '/pages/Ruby_on_Rails', wiki_page_path(:id => 'Ruby_on_Rails') - - get '/my_account' - assert_equal 'wiki_accounts#show', @response.body - assert_equal '/my_account', wiki_account_path - end - - def test_redirect_https - draw do - get 'secure', :to => redirect("/secure/login") - end - - with_https do - get '/secure' - verify_redirect 'https://www.example.com/secure/login' - end - end - - def test_symbolized_path_parameters_is_not_stale - draw do - scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do - get '/', :to => 'countries#index' - get '/cities', :to => 'countries#cities' - end - - get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } - end - - get '/countries/France' - assert_equal 'countries#index', @response.body - - get '/countries/France/cities' - assert_equal 'countries#cities', @response.body - - get '/countries/UK' - verify_redirect 'http://www.example.com/countries/all' - - get '/countries/UK/cities' - verify_redirect 'http://www.example.com/countries/all/cities' - end - - def test_constraints_block_not_carried_to_following_routes - draw do - scope '/italians' do - get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor - get '/sculptors', :to => 'italians#sculptors' - get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/} - end - end - - get '/italians/writers' - assert_equal 'Not Found', @response.body - - get '/italians/sculptors' - assert_equal 'italians#sculptors', @response.body - - get '/italians/painters/botticelli' - assert_equal 'Not Found', @response.body - - get '/italians/painters/michelangelo' - assert_equal 'italians#painters', @response.body - end - - def test_custom_resource_actions_defined_using_string - draw do - resources :customers do - resources :invoices do - get "aged/:months", :on => :collection, :action => :aged, :as => :aged - end - - get "inactive", :on => :collection - post "deactivate", :on => :member - get "old", :on => :collection, :as => :stale - end - end - - get '/customers/inactive' - assert_equal 'customers#inactive', @response.body - assert_equal '/customers/inactive', inactive_customers_path - - post '/customers/1/deactivate' - assert_equal 'customers#deactivate', @response.body - assert_equal '/customers/1/deactivate', deactivate_customer_path(:id => '1') - - get '/customers/old' - assert_equal 'customers#old', @response.body - assert_equal '/customers/old', stale_customers_path - - get '/customers/1/invoices/aged/3' - assert_equal 'invoices#aged', @response.body - assert_equal '/customers/1/invoices/aged/3', aged_customer_invoices_path(:customer_id => '1', :months => '3') - end - - def test_route_defined_in_resources_scope_level - draw do - resources :customers do - get "export" - end - end - - get '/customers/1/export' - assert_equal 'customers#export', @response.body - assert_equal '/customers/1/export', customer_export_path(:customer_id => '1') - end - - def test_named_character_classes_in_regexp_constraints - draw do - get '/purchases/:token/:filename', - :to => 'purchases#fetch', - :token => /[[:alnum:]]{10}/, - :filename => /(.+)/, - :as => :purchase - end - - get '/purchases/315004be7e/Ruby_on_Rails_3.pdf' - assert_equal 'purchases#fetch', @response.body - assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf') - end - - def test_nested_resource_constraints - draw do - resources :lists, :id => /([A-Za-z0-9]{25})|default/ do - resources :todos, :id => /\d+/ - end - end - - get '/lists/01234012340123401234fffff' - assert_equal 'lists#show', @response.body - assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff') - - get '/lists/01234012340123401234fffff/todos/1' - assert_equal 'todos#show', @response.body - assert_equal '/lists/01234012340123401234fffff/todos/1', list_todo_path(:list_id => '01234012340123401234fffff', :id => '1') - - get '/lists/2/todos/1' - assert_equal 'Not Found', @response.body - assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') } - end - - def test_redirect_argument_error - routes = Class.new { include ActionDispatch::Routing::Redirection }.new - assert_raises(ArgumentError) { routes.redirect Object.new } - end - - def test_named_route_check - before, after = nil - - draw do - before = has_named_route?(:hello) - get "/hello", as: :hello, to: "hello#world" - after = has_named_route?(:hello) - end - - assert !before, "expected to not have named route :hello before route definition" - assert after, "expected to have named route :hello after route definition" - end - - def test_explicitly_avoiding_the_named_route - draw do - scope :as => "routes" do - get "/c/:id", :as => :collision, :to => "collision#show" - get "/collision", :to => "collision#show" - get "/no_collision", :to => "collision#show", :as => nil - end - end - - assert !respond_to?(:routes_no_collision_path) - end - - def test_controller_name_with_leading_slash_raise_error - assert_raise(ArgumentError) do - draw { get '/feeds/:service', :to => '/feeds#show' } - end - - assert_raise(ArgumentError) do - draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' } - end - - assert_raise(ArgumentError) do - draw { get '/api/feeds/:service', :to => '/api/feeds#show' } - end - - assert_raise(ArgumentError) do - assert_deprecated do - draw { controller("/feeds") { get '/feeds/:service', :to => :show } } - end - end - - assert_raise(ArgumentError) do - draw { resources :feeds, :controller => '/feeds' } - end - end - - def test_invalid_route_name_raises_error - assert_raise(ArgumentError) do - draw { get '/products', :to => 'products#index', :as => 'products ' } - end - - assert_raise(ArgumentError) do - draw { get '/products', :to => 'products#index', :as => ' products' } - end - - assert_raise(ArgumentError) do - draw { get '/products', :to => 'products#index', :as => 'products!' } - end - - assert_raise(ArgumentError) do - draw { get '/products', :to => 'products#index', :as => 'products index' } - end - - assert_raise(ArgumentError) do - draw { get '/products', :to => 'products#index', :as => '1products' } - end - end - - def test_duplicate_route_name_raises_error - assert_raise(ArgumentError) do - draw do - get '/collision', :to => 'collision#show', :as => 'collision' - get '/duplicate', :to => 'duplicate#show', :as => 'collision' - end - end - end - - def test_duplicate_route_name_via_resources_raises_error - assert_raise(ArgumentError) do - draw do - resources :collisions - get '/collision', :to => 'collision#show', :as => 'collision' - end - end - end - - def test_nested_route_in_nested_resource - draw do - resources :posts, :only => [:index, :show] do - resources :comments, :except => :destroy do - get "views" => "comments#views", :as => :views - end - end - end - - get "/posts/1/comments/2/views" - assert_equal "comments#views", @response.body - assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2') - end - - def test_root_in_deeply_nested_scope - draw do - resources :posts, :only => [:index, :show] do - namespace :admin do - root :to => "index#index" - end - end - end - - get "/posts/1/admin" - assert_equal "admin/index#index", @response.body - assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1') - end - - def test_custom_param - draw do - resources :profiles, :param => :username do - get :details, :on => :member - resources :messages - end - end - - get '/profiles/bob' - assert_equal 'profiles#show', @response.body - assert_equal 'bob', @request.params[:username] - - get '/profiles/bob/details' - assert_equal 'bob', @request.params[:username] - - get '/profiles/bob/messages/34' - assert_equal 'bob', @request.params[:profile_username] - assert_equal '34', @request.params[:id] - end - - def test_custom_param_constraint - draw do - resources :profiles, :param => :username, :username => /[a-z]+/ do - get :details, :on => :member - resources :messages - end - end - - get '/profiles/bob1' - assert_equal 404, @response.status - - get '/profiles/bob1/details' - assert_equal 404, @response.status - - get '/profiles/bob1/messages/34' - assert_equal 404, @response.status - end - - def test_shallow_custom_param - draw do - resources :orders do - constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do - resources :downloads, :param => :download, :shallow => true - end - end - end - - get '/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip' - assert_equal 'downloads#show', @response.body - assert_equal '0c0c0b68-d24b-11e1-a861-001ff3fffe6f', @request.params[:download] - end - - def test_action_from_path_is_not_frozen - draw do - get 'search' => 'search' - end - - get '/search' - assert !@request.params[:action].frozen? - end - - def test_multiple_positional_args_with_the_same_name - draw do - get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false - end - - expected_params = { - controller: 'downloads', - action: 'show', - id: '1' - } - - get '/downloads/1/1.tar' - assert_equal 'downloads#show', @response.body - assert_equal expected_params, @request.symbolized_path_parameters - assert_equal '/downloads/1/1.tar', download_path('1') - assert_equal '/downloads/1/1.tar', download_path('1', '1') - end - - def test_absolute_controller_namespace - draw do - namespace :foo do - get '/', to: '/bar#index', as: 'root' - end - end - - get '/foo' - assert_equal 'bar#index', @response.body - assert_equal '/foo', foo_root_path - end - - def test_namespace_as_controller - draw do - namespace :foo do - get '/', to: '/bar#index', as: 'root' - end - end - - get '/foo' - assert_equal 'bar#index', @response.body - assert_equal '/foo', foo_root_path - end - - def test_trailing_slash - draw do - resources :streams - end - - get '/streams' - assert @response.ok?, 'route without trailing slash should work' - - get '/streams/' - assert @response.ok?, 'route with trailing slash should work' - - get '/streams?foobar' - assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work' - - get '/streams/?foobar' - assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work' - end - - def test_route_with_dashes_in_path - draw do - get '/contact-us', to: 'pages#contact_us' - end - - get '/contact-us' - assert_equal 'pages#contact_us', @response.body - assert_equal '/contact-us', contact_us_path - end - - def test_shorthand_route_with_dashes_in_path - draw do - get '/about-us/index' - end - - get '/about-us/index' - assert_equal 'about_us#index', @response.body - assert_equal '/about-us/index', about_us_index_path - end - - def test_resource_routes_with_dashes_in_path - draw do - resources :photos, only: [:show] do - get 'user-favorites', on: :collection - get 'preview-photo', on: :member - get 'summary-text' - end - end - - get '/photos/user-favorites' - assert_equal 'photos#user_favorites', @response.body - assert_equal '/photos/user-favorites', user_favorites_photos_path - - get '/photos/1/preview-photo' - assert_equal 'photos#preview_photo', @response.body - assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1') - - get '/photos/1/summary-text' - assert_equal 'photos#summary_text', @response.body - assert_equal '/photos/1/summary-text', photo_summary_text_path('1') - - get '/photos/1' - assert_equal 'photos#show', @response.body - assert_equal '/photos/1', photo_path('1') - end - - def test_shallow_path_inside_namespace_is_not_added_twice - draw do - namespace :admin do - shallow do - resources :posts do - resources :comments - end - end - end - end - - get '/admin/posts/1/comments' - assert_equal 'admin/comments#index', @response.body - assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') - end - - def test_mix_string_to_controller_action - draw do - get '/projects', controller: 'project_files', - action: 'index', - to: 'comments#index' - end - get '/projects' - assert_equal 'comments#index', @response.body - end - - def test_mix_string_to_controller - draw do - get '/projects', controller: 'project_files', - to: 'comments#index' - end - get '/projects' - assert_equal 'comments#index', @response.body - end - - def test_mix_string_to_action - draw do - get '/projects', action: 'index', - to: 'comments#index' - end - get '/projects' - assert_equal 'comments#index', @response.body - end - - def test_mix_symbol_to_controller_action - assert_deprecated do - draw do - get '/projects', controller: 'project_files', - action: 'index', - to: :show - end - end - get '/projects' - assert_equal 'project_files#show', @response.body - end - - def test_mix_string_to_controller_action_no_hash - assert_deprecated do - draw do - get '/projects', controller: 'project_files', - action: 'index', - to: 'show' - end - end - get '/projects' - assert_equal 'show#index', @response.body - end - - def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes - draw do - scope shallow_path: 'projects', shallow_prefix: 'project' do - resources :projects do - resources :files, controller: 'project_files', shallow: true - end - end - end - - get '/projects' - assert_equal 'projects#index', @response.body - assert_equal '/projects', projects_path - - get '/projects/new' - assert_equal 'projects#new', @response.body - assert_equal '/projects/new', new_project_path - - post '/projects' - assert_equal 'projects#create', @response.body - - get '/projects/1' - assert_equal 'projects#show', @response.body - assert_equal '/projects/1', project_path('1') - - get '/projects/1/edit' - assert_equal 'projects#edit', @response.body - assert_equal '/projects/1/edit', edit_project_path('1') - - patch '/projects/1' - assert_equal 'projects#update', @response.body - - delete '/projects/1' - assert_equal 'projects#destroy', @response.body - - get '/projects/1/files' - assert_equal 'project_files#index', @response.body - assert_equal '/projects/1/files', project_files_path('1') - - get '/projects/1/files/new' - assert_equal 'project_files#new', @response.body - assert_equal '/projects/1/files/new', new_project_file_path('1') - - post '/projects/1/files' - assert_equal 'project_files#create', @response.body - - get '/projects/files/2' - assert_equal 'project_files#show', @response.body - assert_equal '/projects/files/2', project_file_path('2') - - get '/projects/files/2/edit' - assert_equal 'project_files#edit', @response.body - assert_equal '/projects/files/2/edit', edit_project_file_path('2') - - patch '/projects/files/2' - assert_equal 'project_files#update', @response.body - - delete '/projects/files/2' - assert_equal 'project_files#destroy', @response.body - end - - def test_scope_path_is_copied_to_shallow_path - draw do - scope path: 'foo' do - resources :posts do - resources :comments, shallow: true - end - end - end - - assert_equal '/foo/comments/1', comment_path('1') - end - - def test_scope_as_is_copied_to_shallow_prefix - draw do - scope as: 'foo' do - resources :posts do - resources :comments, shallow: true - end - end - end - - assert_equal '/comments/1', foo_comment_path('1') - end - - def test_scope_shallow_prefix_is_not_overwritten_by_as - draw do - scope as: 'foo', shallow_prefix: 'bar' do - resources :posts do - resources :comments, shallow: true - end - end - end - - assert_equal '/comments/1', bar_comment_path('1') - end - - def test_scope_shallow_path_is_not_overwritten_by_path - draw do - scope path: 'foo', shallow_path: 'bar' do - resources :posts do - resources :comments, shallow: true - end - end - end - - assert_equal '/bar/comments/1', comment_path('1') - end - -private - - def draw(&block) - self.class.stub_controllers do |routes| - @app = routes - @app.default_url_options = { host: 'www.example.com' } - @app.draw(&block) - end - end - - def url_for(options = {}) - @app.url_helpers.url_for(options) - end - - def method_missing(method, *args, &block) - if method.to_s =~ /_(path|url)$/ - @app.url_helpers.send(method, *args, &block) - else - super - end - end - - def with_https - old_https = https? - https! - yield - ensure - https!(old_https) - end - - def verify_redirect(url, status=301) - assert_equal status, @response.status - assert_equal url, @response.headers['Location'] - assert_equal expected_redirect_body(url), @response.body - end - - def expected_redirect_body(url) - %(You are being redirected.) - end -end - -class TestAltApp < ActionDispatch::IntegrationTest - class AltRequest - attr_accessor :path_parameters, :path_info, :script_name - attr_reader :env - - def initialize(env) - @path_parameters = {} - @env = env - @path_info = "/" - @script_name = "" - end - - def request_method - "GET" - end - - def ip - "127.0.0.1" - end - - def x_header - @env["HTTP_X_HEADER"] || "" - end - end - - class XHeader - def call(env) - [200, {"Content-Type" => "text/html"}, ["XHeader"]] - end - end - - class AltApp - def call(env) - [200, {"Content-Type" => "text/html"}, ["Alternative App"]] - end - end - - AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) - AltRoutes.draw do - get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} - get "/" => TestAltApp::AltApp.new - end - - def app - AltRoutes - end - - def test_alt_request_without_header - get "/" - assert_equal "Alternative App", @response.body - end - - def test_alt_request_with_matched_header - get "/", {}, "HTTP_X_HEADER" => "HEADER" - assert_equal "XHeader", @response.body - end - - def test_alt_request_with_unmatched_header - get "/", {}, "HTTP_X_HEADER" => "NON_MATCH" - assert_equal "Alternative App", @response.body - end -end - -class TestAppendingRoutes < ActionDispatch::IntegrationTest - def simple_app(resp) - lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] } - end - - def setup - super - s = self - @app = ActionDispatch::Routing::RouteSet.new - @app.append do - get '/hello' => s.simple_app('fail') - get '/goodbye' => s.simple_app('goodbye') - end - - @app.draw do - get '/hello' => s.simple_app('hello') - end - end - - def test_goodbye_should_be_available - get '/goodbye' - assert_equal 'goodbye', @response.body - end - - def test_hello_should_not_be_overwritten - get '/hello' - assert_equal 'hello', @response.body - end - - def test_missing_routes_are_still_missing - get '/random' - assert_equal 404, @response.status - end -end - -class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest - module ::Admin - class StorageFilesController < ActionController::Base - def index - render :text => "admin/storage_files#index" - end - end - end - - def draw(&block) - @app = ActionDispatch::Routing::RouteSet.new - @app.draw(&block) - end - - def test_missing_controller - ex = assert_raises(ArgumentError) { - draw do - get '/foo/bar', :action => :index - end - } - assert_match(/Missing :controller/, ex.message) - end - - def test_missing_action - ex = assert_raises(ArgumentError) { - assert_deprecated do - draw do - get '/foo/bar', :to => 'foo' - end - end - } - assert_match(/Missing :action/, ex.message) - end - - def test_missing_action_on_hash - ex = assert_raises(ArgumentError) { - draw do - get '/foo/bar', :to => 'foo#' - end - } - assert_match(/Missing :action/, ex.message) - end - - def test_valid_controller_options_inside_namespace - draw do - namespace :admin do - resources :storage_files, :controller => "storage_files" - end - end - - get '/admin/storage_files' - assert_equal "admin/storage_files#index", @response.body - end - - def test_resources_with_valid_namespaced_controller_option - draw do - resources :storage_files, :controller => 'admin/storage_files' - end - - get '/storage_files' - assert_equal "admin/storage_files#index", @response.body - end - - def test_warn_with_ruby_constant_syntax_controller_option - e = assert_raise(ArgumentError) do - draw do - namespace :admin do - resources :storage_files, :controller => "StorageFiles" - end - end - end - - assert_match "'admin/StorageFiles' is not a supported controller name", e.message - end - - def test_warn_with_ruby_constant_syntax_namespaced_controller_option - e = assert_raise(ArgumentError) do - draw do - resources :storage_files, :controller => 'Admin::StorageFiles' - end - end - - assert_match "'Admin::StorageFiles' is not a supported controller name", e.message - end - - def test_warn_with_ruby_constant_syntax_no_colons - e = assert_raise(ArgumentError) do - draw do - resources :storage_files, :controller => 'Admin' - end - end - - assert_match "'Admin' is not a supported controller name", e.message - end -end - -class TestDefaultScope < ActionDispatch::IntegrationTest - module ::Blog - class PostsController < ActionController::Base - def index - render :text => "blog/posts#index" - end - end - end - - DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new - DefaultScopeRoutes.default_scope = {:module => :blog} - DefaultScopeRoutes.draw do - resources :posts - end - - def app - DefaultScopeRoutes - end - - include DefaultScopeRoutes.url_helpers - - def test_default_scope - get '/posts' - assert_equal "blog/posts#index", @response.body - end -end - -class TestHttpMethods < ActionDispatch::IntegrationTest - RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) - RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) - RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) - RFC3648 = %w(ORDERPATCH) - RFC3744 = %w(ACL) - RFC5323 = %w(SEARCH) - RFC4791 = %w(MKCALENDAR) - RFC5789 = %w(PATCH) - - def simple_app(response) - lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } - end - - setup do - s = self - @app = ActionDispatch::Routing::RouteSet.new - - @app.draw do - (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| - match '/' => s.simple_app(method), :via => method.underscore.to_sym - end - end - end - - (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| - test "request method #{method.underscore} can be matched" do - get '/', nil, 'REQUEST_METHOD' => method - assert_equal method, @response.body - end - end -end - -class TestUriPathEscaping < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - get '/:segment' => lambda { |env| - path_params = env['action_dispatch.request.path_parameters'] - [200, { 'Content-Type' => 'text/plain' }, [path_params[:segment]]] - }, :as => :segment - - get '/*splat' => lambda { |env| - path_params = env['action_dispatch.request.path_parameters'] - [200, { 'Content-Type' => 'text/plain' }, [path_params[:splat]]] - }, :as => :splat - end - end - - include Routes.url_helpers - def app; Routes end - - test 'escapes slash in generated path segment' do - assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d') - end - - test 'unescapes recognized path segment' do - get '/a%20b%2Fc+d' - assert_equal 'a b/c+d', @response.body - end - - test 'does not escape slash in generated path splat' do - assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d') - end - - test 'unescapes recognized path splat' do - get '/a%20b/c+d' - assert_equal 'a b/c+d', @response.body - end -end - -class TestUnicodePaths < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - get "/ほげ" => lambda { |env| - [200, { 'Content-Type' => 'text/plain' }, []] - }, :as => :unicode_path - end - end - - include Routes.url_helpers - def app; Routes end - - test 'recognizes unicode path' do - get "/#{Rack::Utils.escape("ほげ")}" - assert_equal "200", @response.code - end -end - -class TestMultipleNestedController < ActionDispatch::IntegrationTest - module ::Foo - module Bar - class BazController < ActionController::Base - def index - render :inline => "<%= url_for :controller => '/pooh', :action => 'index' %>" - end - end - end - end - - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - namespace :foo do - namespace :bar do - get "baz" => "baz#index" - end - end - get "pooh" => "pooh#index" - end - end - - include Routes.url_helpers - def app; Routes end - - test "controller option which starts with '/' from multiple nested controller" do - get "/foo/bar/baz" - assert_equal "/pooh", @response.body - end -end - -class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + private - get "/~user" => ok - get "/young-and-fine" => ok + def draw(&block) + self.class.stub_controllers do |routes| + @app = routes + @app.default_url_options = { host: 'www.example.com' } + @app.draw(&block) end end - include Routes.url_helpers - def app; Routes end - - test 'recognizes tilde path' do - get "/~user" - assert_equal "200", @response.code - end - - test 'recognizes minus path' do - get "/young-and-fine" - assert_equal "200", @response.code + def url_for(options = {}) + @app.url_helpers.url_for(options) end -end - -class TestRedirectInterpolation < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - get "/foo/:id" => redirect("/foo/bar/%{id}") - get "/bar/:id" => redirect(:path => "/foo/bar/%{id}") - get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}") - get "/foo/bar/:id" => ok - get "/baz" => ok + def method_missing(method, *args, &block) + if method.to_s =~ /_(path|url)$/ + @app.url_helpers.send(method, *args, &block) + else + super end end - def app; Routes end - - test "redirect escapes interpolated parameters with redirect proc" do - get "/foo/1%3E" - verify_redirect "http://www.example.com/foo/bar/1%3E" - end - - test "redirect escapes interpolated parameters with option proc" do - get "/bar/1%3E" - verify_redirect "http://www.example.com/foo/bar/1%3E" - end - - test "path redirect escapes interpolated parameters correctly" do - get "/foo/1%201" - verify_redirect "http://www.example.com/foo/bar/1%201" - - get "/baz/1%201" - verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201" + def with_https + old_https = https? + https! + yield + ensure + https!(old_https) end -private def verify_redirect(url, status=301) assert_equal status, @response.status assert_equal url, @response.headers['Location'] @@ -3867,499 +183,3 @@ def expected_redirect_body(url) %(You are being redirected.) end end - -class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == 'foo' } - get "/:bar" => ok - end - end - - def app; Routes end - - test "parameters are reset between constraint checks" do - get "/bar" - assert_equal nil, @request.params[:foo] - assert_equal "bar", @request.params[:bar] - end -end - -class TestGlobRoutingMapper < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - get "/*id" => redirect("/not_cars"), :constraints => {id: /dummy/} - get "/cars" => ok - end - end - - #include Routes.url_helpers - def app; Routes end - - def test_glob_constraint - get "/dummy" - assert_equal "301", @response.code - assert_equal "/not_cars", @response.header['Location'].match('/[^/]+$')[0] - end - - def test_glob_constraint_skip_route - get "/cars" - assert_equal "200", @response.code - end - def test_glob_constraint_skip_all - get "/missing" - assert_equal "404", @response.code - end -end - -class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - get '/foo' => ok, as: :foo - get '/post(/:action(/:id))' => ok, as: :posts - get '/:foo/:foo_type/bars/:id' => ok, as: :bar - get '/projects/:id.:format' => ok, as: :project - get '/pages/:id' => ok, as: :page - get '/wiki/*page' => ok, as: :wiki - end - end - - include Routes.url_helpers - def app; Routes end - - test 'enabled when not mounted and default_url_options is empty' do - assert Routes.url_helpers.optimize_routes_generation? - end - - test 'named route called as singleton method' do - assert_equal '/foo', Routes.url_helpers.foo_path - end - - test 'named route called on included module' do - assert_equal '/foo', foo_path - end - - test 'nested optional segments are removed' do - assert_equal '/post', Routes.url_helpers.posts_path - assert_equal '/post', posts_path - end - - test 'segments with same prefix are replaced correctly' do - assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1') - assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1') - end - - test 'segments separated with a period are replaced correctly' do - assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json) - assert_equal '/projects/1.json', project_path(1, :json) - end - - test 'segments with question marks are escaped' do - assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar') - assert_equal '/pages/foo%3Fbar', page_path('foo?bar') - end - - test 'segments with slashes are escaped' do - assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar') - assert_equal '/pages/foo%2Fbar', page_path('foo/bar') - end - - test 'glob segments with question marks are escaped' do - assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar') - assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar') - end - - test 'glob segments with slashes are not escaped' do - assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar') - assert_equal '/wiki/foo/bar', wiki_path('foo/bar') - end -end - -class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest - class CategoriesController < ActionController::Base - def show - render :text => "categories#show" - end - end - - class ProductsController < ActionController::Base - def show - render :text => "products#show" - end - end - - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - scope :module => "test_named_route_url_helpers" do - get "/categories/:id" => 'categories#show', :as => :category - get "/products/:id" => 'products#show', :as => :product - end - end - end - - def app; Routes end - - include Routes.url_helpers - - test "url helpers do not ignore nil parameters when using non-optimized routes" do - Routes.stubs(:optimize_routes_generation?).returns(false) - - get "/categories/1" - assert_response :success - assert_raises(ActionController::UrlGenerationError) { product_path(nil) } - end -end - -class TestUrlConstraints < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - constraints :subdomain => 'admin' do - get '/' => ok, :as => :admin_root - end - - scope :constraints => { :protocol => 'https://' } do - get '/' => ok, :as => :secure_root - end - - get '/' => ok, :as => :alternate_root, :constraints => { :port => 8080 } - - get '/search' => ok, :constraints => { :subdomain => false } - - get '/logs' => ok, :constraints => { :subdomain => true } - end - end - - include Routes.url_helpers - def app; Routes end - - test "constraints are copied to defaults when using constraints method" do - assert_equal 'http://admin.example.com/', admin_root_url - - get 'http://admin.example.com/' - assert_response :success - end - - test "constraints are copied to defaults when using scope constraints hash" do - assert_equal 'https://www.example.com/', secure_root_url - - get 'https://www.example.com/' - assert_response :success - end - - test "constraints are copied to defaults when using route constraints hash" do - assert_equal 'http://www.example.com:8080/', alternate_root_url - - get 'http://www.example.com:8080/' - assert_response :success - end - - test "false constraint expressions check for absence of values" do - get 'http://example.com/search' - assert_response :success - assert_equal 'http://example.com/search', search_url - - get 'http://api.example.com/search' - assert_response :not_found - end - - test "true constraint expressions check for presence of values" do - get 'http://api.example.com/logs' - assert_response :success - assert_equal 'http://api.example.com/logs', logs_url - - get 'http://example.com/logs' - assert_response :not_found - end -end - -class TestInvalidUrls < ActionDispatch::IntegrationTest - class FooController < ActionController::Base - def show - render :text => "foo#show" - end - end - - test "invalid UTF-8 encoding returns a 400 Bad Request" do - with_routing do |set| - set.draw do - get "/bar/:id", :to => redirect("/foo/show/%{id}") - get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" - get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" - get "/:controller(/:action(/:id))" - end - - get "/%E2%EF%BF%BD%A6" - assert_response :bad_request - - get "/foo/%E2%EF%BF%BD%A6" - assert_response :bad_request - - get "/foo/show/%E2%EF%BF%BD%A6" - assert_response :bad_request - - get "/bar/%E2%EF%BF%BD%A6" - assert_response :bad_request - end - end -end - -class TestOptionalRootSegments < ActionDispatch::IntegrationTest - stub_controllers do |routes| - Routes = routes - Routes.draw do - get '/(page/:page)', :to => 'pages#index', :as => :root - end - end - - def app - Routes - end - - include Routes.url_helpers - - def test_optional_root_segments - get '/' - assert_equal 'pages#index', @response.body - assert_equal '/', root_path - - get '/page/1' - assert_equal 'pages#index', @response.body - assert_equal '1', @request.params[:page] - assert_equal '/page/1', root_path('1') - assert_equal '/page/1', root_path(:page => '1') - end -end - -class TestPortConstraints < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - get '/integer', to: ok, constraints: { :port => 8080 } - get '/string', to: ok, constraints: { :port => '8080' } - get '/array', to: ok, constraints: { :port => [8080] } - get '/regexp', to: ok, constraints: { :port => /8080/ } - end - end - - include Routes.url_helpers - def app; Routes end - - def test_integer_port_constraints - get 'http://www.example.com/integer' - assert_response :not_found - - get 'http://www.example.com:8080/integer' - assert_response :success - end - - def test_string_port_constraints - get 'http://www.example.com/string' - assert_response :not_found - - get 'http://www.example.com:8080/string' - assert_response :success - end - - def test_array_port_constraints - get 'http://www.example.com/array' - assert_response :not_found - - get 'http://www.example.com:8080/array' - assert_response :success - end - - def test_regexp_port_constraints - get 'http://www.example.com/regexp' - assert_response :not_found - - get 'http://www.example.com:8080/regexp' - assert_response :success - end -end - -class TestFormatConstraints < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - - get '/string', to: ok, constraints: { format: 'json' } - get '/regexp', to: ok, constraints: { format: /json/ } - get '/json_only', to: ok, format: true, constraints: { format: /json/ } - get '/xml_only', to: ok, format: 'xml' - end - end - - include Routes.url_helpers - def app; Routes end - - def test_string_format_constraints - get 'http://www.example.com/string' - assert_response :success - - get 'http://www.example.com/string.json' - assert_response :success - - get 'http://www.example.com/string.html' - assert_response :not_found - end - - def test_regexp_format_constraints - get 'http://www.example.com/regexp' - assert_response :success - - get 'http://www.example.com/regexp.json' - assert_response :success - - get 'http://www.example.com/regexp.html' - assert_response :not_found - end - - def test_enforce_with_format_true_with_constraint - get 'http://www.example.com/json_only.json' - assert_response :success - - get 'http://www.example.com/json_only.html' - assert_response :not_found - - get 'http://www.example.com/json_only' - assert_response :not_found - end - - def test_enforce_with_string - get 'http://www.example.com/xml_only.xml' - assert_response :success - - get 'http://www.example.com/xml_only' - assert_response :success - - get 'http://www.example.com/xml_only.json' - assert_response :not_found - end -end - -class TestCallableConstraintValidation < ActionDispatch::IntegrationTest - def test_constraint_with_object_not_callable - assert_raises(ArgumentError) do - ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - get '/test', to: ok, constraints: Object.new - end - end - end - end -end - -class TestRouteDefaults < ActionDispatch::IntegrationTest - stub_controllers do |routes| - Routes = routes - Routes.draw do - resources :posts, bucket_type: 'post' - resources :projects, defaults: { bucket_type: 'project' } - end - end - - def app - Routes - end - - include Routes.url_helpers - - def test_route_options_are_required_for_url_for - assert_raises(ActionController::UrlGenerationError) do - assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, only_path: true) - end - - assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, bucket_type: 'post', only_path: true) - end - - def test_route_defaults_are_not_required_for_url_for - assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true) - end -end - -class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest - stub_controllers do |routes| - Routes = routes - Routes.draw do - rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - mount rack_app, at: '/account', as: 'account' - mount rack_app, at: '/:locale/account', as: 'localized_account' - end - end - - def app - Routes - end - - include Routes.url_helpers - - def test_mounted_application_doesnt_match_unnamed_route - assert_raise(ActionController::UrlGenerationError) do - assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) - end - - assert_raise(ActionController::UrlGenerationError) do - assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) - end - end -end - -class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest - stub_controllers do |routes| - Routes = routes - Routes.draw do - get '/account', to: redirect('/myaccount'), as: 'account' - get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account' - end - end - - def app - Routes - end - - include Routes.url_helpers - - def test_redirect_doesnt_match_unnamed_route - assert_raise(ActionController::UrlGenerationError) do - assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) - end - - assert_raise(ActionController::UrlGenerationError) do - assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) - end - end -end - -class TestUrlGenerationErrors < ActionDispatch::IntegrationTest - Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - get "/products/:id" => 'products#show', :as => :product - end - end - - def app; Routes end - - include Routes.url_helpers - - test "url helpers raise a helpful error message whem generation fails" do - url, missing = { action: 'show', controller: 'products', id: nil }, [:id] - message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" - - # Optimized url helper - error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) } - assert_equal message, error.message - - # Non-optimized url helper - error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } - assert_equal message, error.message - end -end From 687f777b61acf2bac9a877b83b41a00075ac95dc Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Sun, 17 Aug 2014 00:27:56 +0530 Subject: [PATCH 21/23] Fixed route generation for canonical actions --- Gemfile | 2 ++ .../routing/dsl/scope/mount.rb | 6 ++++- .../routing/dsl/singleton_resource.rb | 22 ++++++++++++++++--- actionpack/test/dispatch/new_routing_test.rb | 13 +++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 2a695df618f34..28ba2398b10de 100644 --- a/Gemfile +++ b/Gemfile @@ -96,3 +96,5 @@ end # A gem necessary for ActiveRecord tests with IBM DB gem 'ibm_db' if ENV['IBM_DB'] + +gem 'byebug' diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb index f250b4ed13d06..a8d380386f44b 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mount.rb @@ -109,7 +109,7 @@ def name_for_action(as, action) #:nodoc: # [name_prefix, member_name, prefix] # end - name = [name_prefix, prefix] + name = prefixed_name name_prefix, prefix if candidate = name.select(&:present?).join("_").presence # If a name was not explicitly given, we check if it is valid @@ -122,6 +122,10 @@ def name_for_action(as, action) #:nodoc: end end end + + def prefixed_name(name_prefix, prefix) + [name_prefix, prefix] + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb index e80446aa00ee2..0f217b5cb7652 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb @@ -15,18 +15,34 @@ def initialize(parent, resource, options) end def name - as || @name + @as || @name end def draw post '/', action: :create, as: name - get '/new', action: :new, as: "new_#{name}" - get '/edit', action: :edit, as: "edit_#{name}" + get '/new', action: :new + get '/edit', action: :edit get '/', action: :show patch '/', action: :update put '/', action: :update delete '/', action: :destroy end + + def path_for_action(action, path) #:nodoc: + if canonical_action?(action, path.blank?) + self.path.to_s + else + super + end + end + + def canonical_action?(action, flag) #:nodoc: + flag && CANONICAL_ACTIONS.include?(action.to_s) + end + + def prefixed_name(name_prefix, prefix) + [prefix, name] + end end end end diff --git a/actionpack/test/dispatch/new_routing_test.rb b/actionpack/test/dispatch/new_routing_test.rb index 0a3f0fbb953b2..d33d3466d9205 100644 --- a/actionpack/test/dispatch/new_routing_test.rb +++ b/actionpack/test/dispatch/new_routing_test.rb @@ -2,6 +2,7 @@ require 'erb' require 'abstract_unit' require 'controller/fake_controllers' +require "byebug" class TestRoutingMapper < ActionDispatch::IntegrationTest SprocketsApp = lambda { |env| @@ -143,6 +144,18 @@ def test_session_singleton_resource assert_equal '/session/reset', reset_session_path end + # def test_session_info_nested_singleton_resource + # draw do + # resource :session do + # resource :info + # end + # end + + # get '/session/info' + # assert_equal 'infos#show', @response.body + # assert_equal '/session/info', session_info_path + # end + private def draw(&block) From 2779012e990767145e34a416b8d132065562c552 Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Sun, 17 Aug 2014 00:54:39 +0530 Subject: [PATCH 22/23] Improved prefix naming for singleton resources --- .../routing/dsl/scope/mapping.rb | 2 +- .../routing/dsl/singleton_resource.rb | 12 +- actionpack/test/dispatch/new_routing_test.rb | 289 +++++++++++++++++- 3 files changed, 289 insertions(+), 14 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb index 586573f52f625..8f3fbef41db10 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/mapping.rb @@ -335,7 +335,7 @@ def path_ast(path) parser.parse path end - def dispatcher(defaults) + def dispatcher(defaults={}) @set.dispatcher defaults end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb index 0f217b5cb7652..7f3b21ad1c5be 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb @@ -19,7 +19,7 @@ def name end def draw - post '/', action: :create, as: name + post '/', action: :create get '/new', action: :new get '/edit', action: :edit get '/', action: :show @@ -41,7 +41,15 @@ def canonical_action?(action, flag) #:nodoc: end def prefixed_name(name_prefix, prefix) - [prefix, name] + if @parent.class != Scope + [prefix, @parent.name, name] + else + [prefix, name] + end + end + + def member + yield end end end diff --git a/actionpack/test/dispatch/new_routing_test.rb b/actionpack/test/dispatch/new_routing_test.rb index d33d3466d9205..b38ea823fbcae 100644 --- a/actionpack/test/dispatch/new_routing_test.rb +++ b/actionpack/test/dispatch/new_routing_test.rb @@ -144,17 +144,284 @@ def test_session_singleton_resource assert_equal '/session/reset', reset_session_path end - # def test_session_info_nested_singleton_resource - # draw do - # resource :session do - # resource :info - # end - # end - - # get '/session/info' - # assert_equal 'infos#show', @response.body - # assert_equal '/session/info', session_info_path - # end + def test_session_info_nested_singleton_resource + draw do + resource :session do + resource :info + end + end + + get '/session/info' + assert_equal 'infos#show', @response.body + assert_equal '/session/info', session_info_path + end + + def test_member_on_resource + draw do + resource :session do + member do + get :crush + end + end + end + + get '/session/crush' + assert_equal 'sessions#crush', @response.body + assert_equal '/session/crush', crush_session_path + end + + def test_redirect_modulo + draw do + get 'account/modulo/:name', :to => redirect("/%{name}s") + end + + get '/account/modulo/name' + verify_redirect 'http://www.example.com/names' + end + + def test_redirect_proc + draw do + get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" } + end + + get '/account/proc/person' + verify_redirect 'http://www.example.com/people' + end + + def test_redirect_proc_with_request + draw do + get 'account/proc_req' => redirect {|params, req| "/#{req.method}" } + end + + get '/account/proc_req' + verify_redirect 'http://www.example.com/GET' + end + + def test_redirect_hash_with_subdomain + draw do + get 'mobile', :to => redirect(:subdomain => 'mobile') + end + + get '/mobile' + verify_redirect 'http://mobile.example.com/mobile' + end + + def test_redirect_hash_with_domain_and_path + draw do + get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '') + end + + get '/documentation' + verify_redirect 'http://www.example-documentation.com' + end + + def test_redirect_hash_with_path + draw do + get 'new_documentation', :to => redirect(:path => '/documentation/new') + end + + get '/new_documentation' + verify_redirect 'http://www.example.com/documentation/new' + end + + def test_redirect_hash_with_host + draw do + get 'super_new_documentation', :to => redirect(:host => 'super-docs.com') + end + + get '/super_new_documentation?section=top' + verify_redirect 'http://super-docs.com/super_new_documentation?section=top' + end + + def test_redirect_hash_path_substitution + draw do + get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + end + + get '/stores/iernest' + verify_redirect 'http://stores.example.com/iernest' + end + + def test_redirect_hash_path_substitution_with_catch_all + draw do + get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}') + end + + get '/stores/iernest/products' + verify_redirect 'http://stores.example.com/iernest/products' + end + + def test_redirect_class + draw do + get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector) + end + + get '/youtube_favorites/oHg5SJYRHA0/rick-rolld' + verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0' + end + + def test_openid + draw do + match 'openid/login', :via => [:get, :post], :to => "openid#login" + end + + get '/openid/login' + assert_equal 'openid#login', @response.body + + post '/openid/login' + assert_equal 'openid#login', @response.body + end + + def test_bookmarks + draw do + scope "bookmark", :controller => "bookmarks", :as => :bookmark do + get :new, :path => "build" + post :create, :path => "create", :as => "" + put :update + get :remove, :action => :destroy, :as => :remove + end + end + + get '/bookmark/build' + assert_equal 'bookmarks#new', @response.body + assert_equal '/bookmark/build', bookmark_new_path + + post '/bookmark/create' + assert_equal 'bookmarks#create', @response.body + assert_equal '/bookmark/create', bookmark_path + + put '/bookmark/update' + assert_equal 'bookmarks#update', @response.body + assert_equal '/bookmark/update', bookmark_update_path + + get '/bookmark/remove' + assert_equal 'bookmarks#destroy', @response.body + assert_equal '/bookmark/remove', bookmark_remove_path + end + + def test_pagemarks + draw do + scope "pagemark", :controller => "pagemarks", :as => :pagemark do + get "new", :path => "build" + post "create", :as => "" + put "update" + get "remove", :action => :destroy, :as => :remove + end + end + + get '/pagemark/build' + assert_equal 'pagemarks#new', @response.body + assert_equal '/pagemark/build', pagemark_new_path + + post '/pagemark/create' + assert_equal 'pagemarks#create', @response.body + assert_equal '/pagemark/create', pagemark_path + + put '/pagemark/update' + assert_equal 'pagemarks#update', @response.body + assert_equal '/pagemark/update', pagemark_update_path + + get '/pagemark/remove' + assert_equal 'pagemarks#destroy', @response.body + assert_equal '/pagemark/remove', pagemark_remove_path + end + + def test_admin + draw do + constraints(:ip => /192\.168\.1\.\d\d\d/) do + get 'admin' => "queenbee#index" + end + + constraints ::TestRoutingMapper::IpRestrictor do + get 'admin/accounts' => "queenbee#accounts" + end + + get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor + end + + get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#index', @response.body + + get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + + get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#accounts', @response.body + + get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '192.168.1.100'} + assert_equal 'queenbee#passwords', @response.body + + get '/admin/passwords', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] + end + + def test_global + draw do + controller(:global) do + get 'global/hide_notice' + get 'global/export', :action => :export, :as => :export_request + get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } + get 'global/:action' + end + end + + 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', export_request_path + assert_equal '/global/hide_notice', global_hide_notice_path + assert_equal '/export/123/foo.txt', export_download_path(:id => 123, :file => 'foo.txt') + end + + def test_local + draw do + get "/local/:action", :controller => "local" + end + + get '/local/dashboard' + assert_equal 'local#dashboard', @response.body + end + + # tests the use of dup in url_for + def test_url_for_with_no_side_effects + draw do + get "/projects/status(.:format)" + end + + # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) + original_options = {:controller => 'projects', :action => 'status'} + options = original_options.dup + + url_for options + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_url_for_does_not_modify_controller + draw do + get "/projects/status(.:format)" + end + + controller = '/projects' + options = {:controller => controller, :action => 'status', :only_path => true} + url = url_for(options) + + assert_equal '/projects/status', url + assert_equal '/projects', controller + end private From 22ed9534f7f1e774c7e007b85c792ff0e0ebaaac Mon Sep 17 00:00:00 2001 From: Ujjwal Thaakar Date: Sun, 17 Aug 2014 02:44:20 +0530 Subject: [PATCH 23/23] Added member scope for resources This needs to be a separate class. Need to rethink resources --- .../action_dispatch/routing/dsl/resource.rb | 57 ++++++++++++----- .../routing/dsl/scope/match.rb | 6 +- .../routing/dsl/singleton_resource.rb | 15 ++++- actionpack/test/dispatch/new_routing_test.rb | 63 +++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/dsl/resource.rb b/actionpack/lib/action_dispatch/routing/dsl/resource.rb index 233b5e436b973..3e76498764c74 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/resource.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/resource.rb @@ -1,31 +1,58 @@ +require "action_dispatch/routing/dsl/singleton_resource" + module ActionDispatch module Routing module DSL - class Resource < Scope - # CANONICAL_ACTIONS holds all actions that does not need a prefix or - # a path appended since they fit properly in their scope level. - VALID_ON_OPTIONS = [:new, :collection, :member] - RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] - CANONICAL_ACTIONS = %w(index create new show update destroy) - + class Resource < SingletonResource attr_reader :param def initialize(parent, resource, options) super - @name = resource.to_s - @path = (options[:path] || @name).to_s - @controller = (options[:controller] || @name).to_s @param = (options[:param] || :id).to_sym + @path = @path.pluralize + @name = @name.singularize + end + + def draw + get '/', action: :index + post '/', action: :create + get '/new', action: :new + get '/edit', action: :edit + get "/:#{@param}", action: :show + patch "/:#{@param}", action: :update + put "/:#{@param}", action: :update + delete "/:#{@param}", action: :destroy + end - declare_resourceful_routes + def member + param = ":#{name.singularize}_#{@param}" + @path, old_path = "/#{@path}/#{param}", @path + yield + @path = old_path end - def name - @path + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + add_route(path, options) + end end - def declare_resourceful_routes - + def prefixed_name(name_prefix, prefix) + if @parent.class != Scope + [prefix, @parent.name, name] + else + if prefix.blank? + if has_named_route?(name.pluralize) + [name] + else + [name.pluralize] + end + else + [prefix, name] + end + end end end end diff --git a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb index b907d69b964b5..f438883631582 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/scope/match.rb @@ -190,12 +190,16 @@ def match(*args) route_options[:to].tr!("-", "_") end - add_route(path, route_options) + decomposed_match(path, route_options) end self end + def decomposed_match(path, options) # :nodoc: + add_route(path, options) + end + # You can specify what Rails should route "/" to with the root method: # # root to: 'pages#main' diff --git a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb index 7f3b21ad1c5be..8906de312a8db 100644 --- a/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb +++ b/actionpack/lib/action_dispatch/routing/dsl/singleton_resource.rb @@ -11,7 +11,7 @@ def initialize(parent, resource, options) super @name = resource.to_s @path = (@path || @name).to_s - @controller = (@controller || @name).pluralize.to_s + @controller = (@controller || @name.to_s.pluralize).to_s end def name @@ -51,6 +51,19 @@ def prefixed_name(name_prefix, prefix) def member yield end + + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + member { add_route(path, options) } + end + end + + def name_for_action(as, action) #:nodoc: + return nil unless as || action + super + end end end end diff --git a/actionpack/test/dispatch/new_routing_test.rb b/actionpack/test/dispatch/new_routing_test.rb index b38ea823fbcae..019a83eeffdeb 100644 --- a/actionpack/test/dispatch/new_routing_test.rb +++ b/actionpack/test/dispatch/new_routing_test.rb @@ -423,6 +423,69 @@ def test_url_for_does_not_modify_controller assert_equal '/projects', controller end + # tests the arguments modification free version of define_hash_access + def test_named_route_with_no_side_effects + draw do + resources :customers do + get "profile", :on => :member + end + end + + original_options = { :host => 'test.host' } + options = original_options.dup + + profile_customer_url("customer_model", options) + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_projects_status + draw do + get "/projects/status(.:format)" + end + + assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true) + assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true) + end + + def test_projects + draw do + resources :projects, :controller => :project + end + + get '/projects' + assert_equal 'project#index', @response.body + assert_equal '/projects', projects_path + + post '/projects' + assert_equal 'project#create', @response.body + + get '/projects.xml' + assert_equal 'project#index', @response.body + assert_equal '/projects.xml', projects_path(:format => 'xml') + + get '/projects/new' + assert_equal 'project#new', @response.body + assert_equal '/projects/new', new_project_path + + get '/projects/new.xml' + assert_equal 'project#new', @response.body + assert_equal '/projects/new.xml', new_project_path(:format => 'xml') + + get '/projects/1' + assert_equal 'project#show', @response.body + assert_equal '/projects/1', project_path(:id => '1') + + get '/projects/1.xml' + assert_equal 'project#show', @response.body + assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml') + + get '/projects/1/edit' + assert_equal 'project#edit', @response.body + assert_equal '/projects/1/edit', edit_project_path(:id => '1') + end + private def draw(&block)