diff options
100 files changed, 1467 insertions, 1899 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 897bdeff83..270e96c647 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,44 +13,44 @@ GIT GIT remote: git://github.com/mikel/mail.git - revision: b159e0a542962fdd5e292a48cfffa560d7cf412e + revision: 64ef1a12efcdda53fd63e1456c2c564044bf82ce specs: mail (2.6.3.edge) mime-types (>= 1.16, < 3) GIT remote: git://github.com/rails/arel.git - revision: aac9da257f291ad8d2d4f914528881c240848bb2 + revision: d5432b4616ff43fbb14540d351eed351e21bb20e branch: master specs: arel (7.0.0.alpha) GIT remote: git://github.com/rails/globalid.git - revision: 4df66fb9e9f0c832d29119aa8bc30be55a614b71 + revision: 8178ff2dc898a8f49dd71f6eb46f2a09712462de specs: - globalid (0.3.5) + globalid (0.3.6) activesupport (>= 4.1.0) GIT remote: git://github.com/rails/jquery-rails.git - revision: 272abdd319bb3381b23182b928b25320590096b0 + revision: 38053f45402f1ccc4cce5dd8c7005ec707376db3 branch: master specs: - jquery-rails (4.0.3) + jquery-rails (4.0.4) rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) GIT remote: git://github.com/rails/sprockets-rails.git - revision: 85b89c44ad40af3056899808475e6e4bf65c1f5a + revision: ad4a43bd1bb19c86a8bf94a2ad5e477686161490 branch: master specs: - sprockets-rails (3.0.0.beta1) + sprockets-rails (3.0.0) actionpack (>= 4.0) activesupport (>= 4.0) - sprockets (>= 3.0.0, < 4.0) + sprockets (>= 3.0.0) PATH remote: . @@ -113,74 +113,70 @@ GEM remote: https://rubygems.org/ specs: amq-protocol (1.9.2) - backburner (0.4.6) - beaneater (~> 0.3.1) - dante (~> 0.1.5) + backburner (1.0.0) + beaneater (~> 1.0) + dante (> 0.1.5) bcrypt (3.1.10) bcrypt (3.1.10-x64-mingw32) bcrypt (3.1.10-x86-mingw32) - beaneater (0.3.3) - benchmark-ips (2.1.1) + beaneater (1.0.0) + benchmark-ips (2.3.0) builder (3.2.2) bunny (1.7.0) amq-protocol (>= 1.9.2) - byebug (4.0.5) + byebug (5.0.0) columnize (= 0.9.0) - celluloid (0.16.0) + celluloid (0.16.1) timers (~> 4.0.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.3.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.0) + coffee-script-source (1.9.1.1) columnize (0.9.0) - concurrent-ruby (0.9.0) - connection_pool (2.1.1) - dalli (2.7.2) - dante (0.1.5) + concurrent-ruby (0.9.1) + connection_pool (2.2.0) + dalli (2.7.4) + dante (0.2.0) delayed_job (4.0.6) activesupport (>= 3.0, < 5.0) delayed_job_active_record (4.0.3) activerecord (>= 3.0, < 5.0) delayed_job (>= 3.0, < 4.1) erubis (2.7.0) - execjs (2.3.0) + execjs (2.5.2) hitimes (1.2.2) hitimes (1.2.2-x86-mingw32) i18n (0.7.0) - json (1.8.2) + json (1.8.3) kindlerb (0.1.1) mustache nokogiri - loofah (2.0.1) + loofah (2.0.2) nokogiri (>= 1.5.9) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.4.3) + mime-types (2.6.1) mini_portile (0.6.2) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - multi_json (1.11.0) - mustache (1.0.0) + multi_json (1.11.2) + mustache (1.0.2) mysql (2.9.1) - mysql2 (0.3.18) + mysql2 (0.3.19) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - nokogiri (1.6.6.2-x64-mingw32) - mini_portile (~> 0.6.0) - nokogiri (1.6.6.2-x86-mingw32) - mini_portile (~> 0.6.0) - pg (0.18.1) + pg (0.18.2) psych (2.0.13) - que (0.9.2) + que (0.10.0) queue_classic (3.1.0) pg (>= 0.17, < 0.19) racc (1.4.12) - rack (1.6.0) + rack (1.6.4) rack-cache (1.2) rack (>= 0.4) rack-protection (1.5.3) @@ -199,7 +195,7 @@ GEM rdoc (4.2.0) redcarpet (3.2.3) redis (3.2.1) - redis-namespace (1.5.1) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.25.2) mono_logger (~> 1.0) @@ -212,47 +208,46 @@ GEM redis (~> 3.0) resque (~> 1.25) rufus-scheduler (~> 3.0) - rufus-scheduler (3.0.9) - tzinfo + rufus-scheduler (3.1.3) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sequel (4.19.0) + sequel (4.25.0) serverengine (1.5.10) sigdump (~> 0.2.2) - sidekiq (3.3.2) - celluloid (>= 0.16.0) - connection_pool (>= 2.1.1) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) - sigdump (0.2.2) - sinatra (1.4.5) + sidekiq (3.4.2) + celluloid (~> 0.16.0) + connection_pool (~> 2.2, >= 2.2.0) + json (~> 1.0) + redis (~> 3.2, >= 3.2.1) + redis-namespace (~> 1.5, >= 1.5.2) + sigdump (0.2.3) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) sneakers (1.0.4) bunny (~> 1.7.0) serverengine (~> 1.5.5) thor thread (~> 0.1.7) - sprockets (3.0.2) + sprockets (3.0.3) rack (~> 1.0) sqlite3 (1.3.10) stackprof (0.2.7) - sucker_punch (1.3.2) + sucker_punch (1.5.0) celluloid (~> 0.16.0) thor (0.19.1) thread (0.1.7) thread_safe (0.3.5) - tilt (1.4.1) + tilt (2.0.1) timers (4.0.1) hitimes turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.7.0) + uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) vegas (0.1.11) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 6a68c057ac..8eea4ccd41 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,21 @@ +* Using strings or symbols for middleware class names is deprecated. Convert + things like this: + + middleware.use "Foo::Bar" + + to this: + + middleware.use Foo::Bar + +* ActionController::TestSession now accepts a default value as well as + a block for generating a default value based off the key provided. + + This fixes calls to session#fetch in ApplicationController instances that + take more two arguments or a block from raising `ArgumentError: wrong + number of arguments (2 for 1)` when performing controller tests. + + *Matthew Gerrior* + * Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by default block. diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 17371a5392..55734b9774 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -252,7 +252,7 @@ module ActionController # Define some internal variables that should not be propagated to the view. PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ - :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, + :@_status, :@_headers, :@_params, :@_response, :@_request, :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] def _protected_ivars # :nodoc: diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index ae111e4951..914b0d4b30 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' +require 'active_support/deprecation' module ActionController # Extend ActionDispatch middleware stack to make it aware of options @@ -11,22 +12,14 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(klass, *args, &block) - options = args.extract_options! - @only = Array(options.delete(:only)).map(&:to_s) - @except = Array(options.delete(:except)).map(&:to_s) - args << options unless options.empty? - super + def initialize(klass, args, actions, strategy, block) + @actions = actions + @strategy = strategy + super(klass, args, block) end def valid?(action) - if @only.present? - @only.include?(action) - elsif @except.present? - !@except.include?(action) - else - true - end + @strategy.call @actions, action end end @@ -37,6 +30,32 @@ module ActionController middleware.valid?(action) ? middleware.build(a) : a end end + + private + + INCLUDE = ->(list, action) { list.include? action } + EXCLUDE = ->(list, action) { !list.include? action } + NULL = ->(list, action) { true } + + def build_middleware(klass, args, block) + options = args.extract_options! + only = Array(options.delete(:only)).map(&:to_s) + except = Array(options.delete(:except)).map(&:to_s) + args << options unless options.empty? + + strategy = NULL + list = nil + + if only.any? + strategy = INCLUDE + list = only + elsif except.any? + strategy = EXCLUDE + list = except + end + + Middleware.new(get_class(klass), args, list, strategy, block) + end end # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a @@ -98,11 +117,10 @@ module ActionController class Metal < AbstractController::Base abstract! - attr_internal_writer :env - def env - @_env ||= {} + @_request.env end + deprecate :env # Returns the last part of the controller's name, underscored, without the ending # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. @@ -197,8 +215,7 @@ module ActionController def set_request!(request) #:nodoc: @_request = request - @_env = request.env - @_env['action_controller.instance'] = self + @_request.controller_instance = self end def to_a #:nodoc: @@ -232,13 +249,13 @@ module ActionController end # Returns a Rack endpoint for the given action name. - def self.action(name, klass = ActionDispatch::Request) + def self.action(name) if middleware_stack.any? middleware_stack.build(name) do |env| - new.dispatch(name, klass.new(env)) + new.dispatch(name, ActionDispatch::Request.new(env)) end else - lambda { |env| new.dispatch(name, klass.new(env)) } + lambda { |env| new.dispatch(name, ActionDispatch::Request.new(env)) } end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 9e09242872..d21a778d8d 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -139,7 +139,7 @@ module ActionController #:nodoc: request.session = NullSessionHash.new(request.env) request.env['action_dispatch.request.flash_hash'] = nil request.env['rack.session.options'] = { skip: true } - request.env['action_dispatch.cookies'] = NullCookieJar.build(request) + request.cookie_jar = NullCookieJar.build(request, {}) end protected @@ -160,14 +160,6 @@ module ActionController #:nodoc: end class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc: - def self.build(request) - key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY] - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options_for_env({})) - end - def write(*) # nothing end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index af31de1f3a..a6115674aa 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -199,7 +199,7 @@ module ActionController #:nodoc: def _process_options(options) #:nodoc: super if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" + if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb deleted file mode 100644 index 437fec3dc6..0000000000 --- a/actionpack/lib/action_controller/middleware.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController - class Middleware < Metal - class ActionMiddleware - def initialize(controller, app) - @controller, @app = controller, app - end - - def call(env) - request = ActionDispatch::Request.new(env) - @controller.build(@app).dispatch(:index, request) - end - end - - class << self - alias build new - - def new(app) - ActionMiddleware.new(self, app) - end - end - - attr_internal :app - - def process(action) - response = super - self.status, self.headers, self.response_body = response if response.is_a?(Array) - response - end - - def initialize(app) - super() - @_app = app - end - - def index - call(env) - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index f922e134f7..39069f7378 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -44,12 +44,12 @@ module ActionController def assign_parameters(routes, controller_path, action, parameters = {}) parameters = parameters.symbolize_keys - generated_path, extra_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action)) + generated_path, query_string_keys = routes.generate_extras(parameters.merge(:controller => controller_path, :action => action)) non_path_parameters = {} path_parameters = {} parameters.each do |key, value| - if extra_keys.include?(key) || key == :action || key == :controller + if query_string_keys.include?(key) || key == :action || key == :controller non_path_parameters[key] = value else if value.is_a?(Array) @@ -171,6 +171,10 @@ module ActionController clear end + def fetch(*args, &block) + @data.fetch(*args, &block) + end + private def load! diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 6985cec5f5..de28cd0998 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -34,12 +34,14 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA ].freeze + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP + HTTP_X_FORWARDED_FOR HTTP_VERSION + ].freeze ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset - @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + @env["#{env}".freeze] # @env["HTTP_ACCEPT_CHARSET".freeze] end # end METHOD end @@ -103,13 +105,17 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= check_method(env["REQUEST_METHOD"]) + @request_method ||= check_method(super) end def routes # :nodoc: env["action_dispatch.routes".freeze] end + def routes=(routes) # :nodoc: + env["action_dispatch.routes".freeze] = routes + end + def original_script_name # :nodoc: env['ORIGINAL_SCRIPT_NAME'.freeze] end @@ -118,12 +124,31 @@ module ActionDispatch env[_routes.env_key] end + def engine_script_name=(name) # :nodoc: + env[routes.env_key] = name.dup + end + def request_method=(request_method) #:nodoc: if check_method(request_method) @request_method = env["REQUEST_METHOD"] = request_method end end + def controller_instance # :nodoc: + env['action_controller.instance'.freeze] + end + + def controller_instance=(controller) # :nodoc: + env['action_controller.instance'.freeze] = controller + end + + def show_exceptions? # :nodoc: + # We're treating `nil` as "unset", and we want the default setting to be + # `true`. This logic should be extracted to `env_config` and calculated + # once. + !(env['action_dispatch.show_exceptions'.freeze] == false) + end + # Returns a symbol form of the #request_method def request_method_symbol HTTP_METHOD_LOOKUP[request_method] @@ -210,6 +235,10 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end + def remote_ip=(remote_ip) + @env["action_dispatch.remote_ip".freeze] = remote_ip + end + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: # Returns the unique request id, which is based on either the X-Request-Id header that can @@ -318,6 +347,14 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end + def request_parameters=(params) + env["action_dispatch.request.request_parameters".freeze] = params + end + + def logger + env["action_dispatch.logger".freeze] + end + private def check_method(name) HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 540e11a4a0..a221f4c5af 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -28,7 +28,13 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile @original_filename = hash[:filename] - @original_filename &&= @original_filename.encode "UTF-8" + if @original_filename + begin + @original_filename.encode!(Encoding::UTF_8) + rescue EncodingError + @original_filename.force_encoding(Encoding::UTF_8) + end + end @content_type = hash[:type] @headers = hash[:head] end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 07d97bd6bd..cf4f654ed6 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -8,8 +8,50 @@ require 'active_support/json' module ActionDispatch class Request < Rack::Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(env, host, ssl?, cookies) + env['action_dispatch.cookies'.freeze] ||= Cookies::CookieJar.build(self, cookies) end + + # :stopdoc: + def have_cookie_jar? + env.key? 'action_dispatch.cookies'.freeze + end + + def cookie_jar=(jar) + env['action_dispatch.cookies'.freeze] = jar + end + + def key_generator + env[Cookies::GENERATOR_KEY] + end + + def signed_cookie_salt + env[Cookies::SIGNED_COOKIE_SALT] + end + + def encrypted_cookie_salt + env[Cookies::ENCRYPTED_COOKIE_SALT] + end + + def encrypted_signed_cookie_salt + env[Cookies::ENCRYPTED_SIGNED_COOKIE_SALT] + end + + def secret_token + env[Cookies::SECRET_TOKEN] + end + + def secret_key_base + env[Cookies::SECRET_KEY_BASE] + end + + def cookies_serializer + env[Cookies::COOKIES_SERIALIZER] + end + + def cookies_digest + env[Cookies::COOKIES_DIGEST] + end + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -118,7 +160,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -138,10 +180,10 @@ module ActionDispatch # cookies.signed[:discount] # => 45 def signed @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacySignedCookieJar.new(self) else - SignedCookieJar.new(self, @key_generator, @options) + SignedCookieJar.new(self) end end @@ -161,10 +203,10 @@ module ActionDispatch # cookies.encrypted[:discount] # => 45 def encrypted @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + if upgrade_legacy_signed_cookies? + UpgradeLegacyEncryptedCookieJar.new(self) else - EncryptedCookieJar.new(self, @key_generator, @options) + EncryptedCookieJar.new(self) end end @@ -172,12 +214,26 @@ module ActionDispatch # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end + + protected + + def request; @parent_jar.request; end + + private + + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end + + def key_generator + request.key_generator + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -187,7 +243,7 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -216,34 +272,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(env, host, secure, cookies) - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - new(key_generator, host, secure, options).tap do |hash| + def self.build(req, cookies) + new(req).tap do |hash| hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -292,12 +332,12 @@ module ActionDispatch # if host is not ip and matches domain regexp # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } + options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') } end end @@ -356,27 +396,20 @@ module ActionDispatch @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} - end - mattr_accessor :always_write_cookie self.always_write_cookie = false private def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie + request.ssl? || !cookie[:secure] || always_write_cookie end end class PermanentCookieJar #:nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) @@ -410,7 +443,7 @@ module ActionDispatch protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) @@ -430,7 +463,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -442,7 +475,7 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || 'SHA1' end end @@ -450,10 +483,9 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:signed_cookie_salt]) + secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -505,16 +537,16 @@ module ActionDispatch include ChainedCookieJars include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) + @parent_jar = parent_jar + if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end - @parent_jar = parent_jar - @options = options - secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) - sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + secret = key_generator.generate_key(request.encrypted_cookie_salt || '') + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end @@ -568,9 +600,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 9082aac271..226a688fe2 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -44,6 +44,7 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers['X-Cascade'] == 'pass' @@ -53,14 +54,15 @@ module ActionDispatch response rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false + raise exception unless request.show_exceptions? render_exception(env, exception) end private def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) log_error(env, wrapper) if env['action_dispatch.show_detailed_exceptions'] diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 8c3d45584d..039efc3af8 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -31,10 +31,10 @@ module ActionDispatch 'ActionView::Template::Error' => 'template_error' ) - attr_reader :env, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :line_number, :file - def initialize(env, exception) - @env = env + def initialize(backtrace_cleaner, exception) + @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) @@ -125,10 +125,6 @@ module ActionDispatch end end - def backtrace_cleaner - @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] - end - def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 59639a010e..23da169b22 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -8,6 +8,14 @@ module ActionDispatch def flash @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"]) end + + def flash=(flash) + @env[Flash::KEY] = flash + end + + def flash_hash # :nodoc: + @env[Flash::KEY] + end end # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed @@ -263,14 +271,15 @@ module ActionDispatch end def call(env) + req = ActionDispatch::Request.new env @app.call(env) ensure session = Request::Session.find(env) || {} - flash_hash = env[KEY] + flash_hash = req.flash_hash if flash_hash && (flash_hash.present? || session.key?('flash')) session["flash"] = flash_hash.to_session_value - env[KEY] = flash_hash.dup + req.flash = flash_hash.dup end if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 2617956c74..e65279a285 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -26,30 +26,29 @@ module ActionDispatch end def call(env) - default = env["action_dispatch.request.request_parameters"] - env["action_dispatch.request.request_parameters"] = parse_formatted_parameters(env, @parsers, default) + request = Request.new(env) + + request.request_parameters = parse_formatted_parameters(request, @parsers) @app.call(env) end private - def parse_formatted_parameters(env, parsers, default) - request = Request.new(env) - - return default if request.content_length.zero? + def parse_formatted_parameters(request, parsers) + return if request.content_length.zero? - strategy = parsers.fetch(request.content_mime_type) { return default } + strategy = parsers.fetch(request.content_mime_type) { return nil } strategy.call(request.raw_post) rescue => e # JSON or Ruby code block errors - logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" + logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" raise ParseError.new(e.message, e) end - def logger(env) - env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) + def logger(request) + request.logger || ActiveSupport::Logger.new($stderr) end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 9f894e2ec6..aee2334da9 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -74,16 +74,17 @@ module ActionDispatch # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) - env["action_dispatch.remote_ip"] = GetIp.new(env, check_ip, proxies) - @app.call(env) + req = ActionDispatch::Request.new env + req.remote_ip = GetIp.new(req, check_ip, proxies) + @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp - def initialize(env, check_ip, proxies) - @env = env + def initialize(req, check_ip, proxies) + @req = req @check_ip = check_ip @proxies = proxies end @@ -108,11 +109,11 @@ module ActionDispatch # the last address left, which was presumably set by one of those proxies. def calculate_ip # Set by the Rack web server, this is a single value. - remote_addr = ips_from('REMOTE_ADDR').last + remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. - client_ips = ips_from('HTTP_CLIENT_IP').reverse - forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse + client_ips = ips_from(@req.client_ip).reverse + forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. # If they are both set, it means that this request passed through two @@ -123,8 +124,8 @@ module ActionDispatch if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " + - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: @@ -147,8 +148,9 @@ module ActionDispatch protected def ips_from(header) + return [] unless header # Split the comma-separated list into an array of strings - ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + ips = header.strip.split(/[,\s]+/) ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f0779279c1..12d8dab8eb 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -27,19 +27,21 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env @app.call(env) rescue Exception => exception - if env['action_dispatch.show_exceptions'] == false - raise exception - else + if request.show_exceptions? render_exception(env, exception) + else + raise exception end end private def render_exception(env, exception) - wrapper = ExceptionWrapper.new(env, exception) + backtrace_cleaner = env['action_dispatch.backtrace_cleaner'] + wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception env["action_dispatch.original_path"] = env["PATH_INFO"] diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index bbf734f103..0430ce3b9a 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -4,36 +4,15 @@ require "active_support/dependencies" module ActionDispatch class MiddlewareStack class Middleware - attr_reader :args, :block, :name, :classcache + attr_reader :args, :block, :klass - def initialize(klass_or_name, *args, &block) - @klass = nil - - if klass_or_name.respond_to?(:name) - @klass = klass_or_name - @name = @klass.name - else - @name = klass_or_name.to_s - end - - @classcache = ActiveSupport::Dependencies::Reference - @args, @block = args, block + def initialize(klass, args, block) + @klass = klass + @args = args + @block = block end - def klass - @klass || classcache[@name] - end - - def ==(middleware) - case middleware - when Middleware - klass == middleware.klass - when Class - klass == middleware - else - normalize(@name) == normalize(middleware) - end - end + def name; klass.name; end def inspect klass.to_s @@ -42,12 +21,6 @@ module ActionDispatch def build(app) klass.new(app, *args, &block) end - - private - - def normalize(object) - object.to_s.strip.sub(/^::/, '') - end end include Enumerable @@ -75,19 +48,17 @@ module ActionDispatch middlewares[i] end - def unshift(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.unshift(middleware) + def unshift(klass, *args, &block) + middlewares.unshift(build_middleware(klass, args, block)) end def initialize_copy(other) self.middlewares = other.middlewares.dup end - def insert(index, *args, &block) + def insert(index, klass, *args, &block) index = assert_index(index, :before) - middleware = self.class::Middleware.new(*args, &block) - middlewares.insert(index, middleware) + middlewares.insert(index, build_middleware(klass, args, block)) end alias_method :insert_before, :insert @@ -104,26 +75,48 @@ module ActionDispatch end def delete(target) - middlewares.delete target + target = get_class target + middlewares.delete_if { |m| m.klass == target } end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - middlewares.push(middleware) + def use(klass, *args, &block) + middlewares.push(build_middleware(klass, args, block)) end - def build(app = nil, &block) - app ||= block - raise "MiddlewareStack#build requires an app" unless app + def build(app = Proc.new) middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : middlewares.index(index) + index = get_class index + i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index } raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end + + private + + def get_class(klass) + if klass.is_a?(String) || klass.is_a?(Symbol) + classcache = ActiveSupport::Dependencies::Reference + converted_klass = classcache[klass.to_s] + ActiveSupport::Deprecation.warn <<-eowarn +Passing strings or symbols to the middleware builder is deprecated, please change +them to actual class references. For example: + + "#{klass}" => #{converted_klass} + + eowarn + converted_klass + else + klass + end + end + + def build_middleware(klass, args, block) + Middleware.new(get_class(klass), args, block) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index f38da4fdf6..9462ae4278 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -48,26 +48,30 @@ module ActionDispatch end def call(env) - path = env['PATH_INFO'] + serve ActionDispatch::Request.new env + end + + def serve(request) + path = request.path_info gzip_path = gzip_file_path(path) - if gzip_path && gzip_encoding_accepted?(env) - env['PATH_INFO'] = gzip_path - status, headers, body = @file_server.call(env) + if gzip_path && gzip_encoding_accepted?(request) + request.path_info = gzip_path + status, headers, body = @file_server.call(request.env) if status == 304 return [status, headers, body] end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else - status, headers, body = @file_server.call(env) + status, headers, body = @file_server.call(request.env) end headers['Vary'] = 'Accept-Encoding' if gzip_path return [status, headers, body] ensure - env['PATH_INFO'] = path + request.path_info = path end private @@ -79,8 +83,8 @@ module ActionDispatch ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze) end - def gzip_encoding_accepted?(env) - env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i + def gzip_encoding_accepted?(request) + request.accept_encoding =~ /\bgzip\b/i end def gzip_file_path(path) @@ -110,16 +114,17 @@ module ActionDispatch end def call(env) - case env['REQUEST_METHOD'] - when 'GET', 'HEAD' - path = env['PATH_INFO'].chomp('/'.freeze) + req = ActionDispatch::Request.new env + + if req.get? || req.head? + path = req.path_info.chomp('/'.freeze) if match = @file_handler.match?(path) - env['PATH_INFO'] = match - return @file_handler.call(env) + req.path_info = match + return @file_handler.serve(req) end end - @app.call(env) + @app.call(req.env) end end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 64352d5742..ab13fb14ce 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -16,7 +16,10 @@ module ActionDispatch class Constraints < Endpoint #:nodoc: attr_reader :app, :constraints - def initialize(app, constraints, dispatcher_p) + SERVE = ->(app, req) { app.serve req } + CALL = ->(app, req) { app.call req.env } + + def initialize(app, constraints, strategy) # 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 @@ -26,12 +29,12 @@ module ActionDispatch app = app.app end - @dispatcher = dispatcher_p + @strategy = strategy @app, @constraints, = app, constraints end - def dispatcher?; @dispatcher; end + def dispatcher?; @strategy == SERVE; end def matches?(req) @constraints.all? do |constraint| @@ -43,11 +46,7 @@ module ActionDispatch def serve(req) return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) - if dispatcher? - @app.serve req - else - @app.call req.env - end + @strategy.call @app, req end private @@ -241,11 +240,11 @@ module ActionDispatch def app(blocks) if to.respond_to?(:call) - Constraints.new(to, blocks, false) + Constraints.new(to, blocks, Constraints::CALL) elsif blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - dispatcher(defaults) + dispatcher(defaults.key?(:controller)) end end @@ -335,8 +334,8 @@ module ActionDispatch parser.parse path end - def dispatcher(defaults) - @set.dispatcher defaults + def dispatcher(raise_on_name_error) + @set.dispatcher raise_on_name_error end end @@ -1107,7 +1106,7 @@ module ActionDispatch end def resource_scope - { :controller => controller } + controller end alias :collection_scope :path @@ -1398,7 +1397,7 @@ module ActionDispatch end with_scope_level(:collection) do - scope(parent_resource.collection_scope) do + path_scope(parent_resource.collection_scope) do yield end end @@ -1422,9 +1421,11 @@ module ActionDispatch with_scope_level(:member) do if shallow? - shallow_scope(parent_resource.member_scope) { yield } + shallow_scope { + path_scope(parent_resource.member_scope) { yield } + } else - scope(parent_resource.member_scope) { yield } + path_scope(parent_resource.member_scope) { yield } end end end @@ -1435,7 +1436,7 @@ module ActionDispatch end with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do + path_scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -1448,9 +1449,15 @@ module ActionDispatch with_scope_level(:nested) do if shallow? && shallow_nesting_depth >= 1 - shallow_scope(parent_resource.nested_scope, nested_options) { yield } + shallow_scope do + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end + end else - scope(parent_resource.nested_scope, nested_options) { yield } + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end end end end @@ -1465,9 +1472,10 @@ module ActionDispatch end def shallow - scope(:shallow => true) do - yield - end + @scope = @scope.new(shallow: true) + yield + ensure + @scope = @scope.parent end def shallow? @@ -1684,7 +1692,7 @@ module ActionDispatch @nesting.push(resource) with_scope_level(kind) do - scope(parent_resource.resource_scope) { yield } + controller_scope(parent_resource.resource_scope) { yield } end ensure @nesting.pop @@ -1720,12 +1728,12 @@ module ActionDispatch resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope(path, options = {}) #:nodoc: + def shallow_scope #:nodoc: scope = { :as => @scope[:shallow_prefix], :path => @scope[:shallow_path] } @scope = @scope.new scope - scope(path, options) { yield } + yield ensure @scope = @scope.parent end @@ -1796,6 +1804,21 @@ module ActionDispatch def api_only? @set.api_only? end + private + + def controller_scope(controller) + @scope = @scope.new(controller: controller) + yield + ensure + @scope = @scope.parent + end + + def path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end end # Routing Concerns allow you to declare common routes that can be reused @@ -1971,10 +1994,6 @@ module ActionDispatch def [](key) @hash.fetch(key) { @parent[key] } end - - def []=(k,v) - @hash[k] = v - end end def initialize(set) #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3c1c4fadf6..d6987f4d09 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -24,7 +24,7 @@ module ActionDispatch def serve(req) req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) - + unless uri.host if relative_path?(uri.path) uri.path = "#{req.script_name}/#{uri.path}" @@ -32,7 +32,7 @@ module ActionDispatch uri.path = req.script_name.empty? ? "/" : req.script_name end end - + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -124,7 +124,7 @@ module ActionDispatch url_options[:script_name] = request.script_name end end - + ActionDispatch::Http::URL.url_for url_options end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index c968381c26..2fe61c7aa6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -21,8 +21,8 @@ module ActionDispatch alias inspect to_s class Dispatcher < Routing::Endpoint - def initialize(defaults) - @defaults = defaults + def initialize(raise_on_name_error) + @raise_on_name_error = raise_on_name_error @controller_class_names = ThreadSafe::Cache.new end @@ -34,12 +34,11 @@ module ActionDispatch prepare_params!(params) - # Just raise undefined constant errors if a controller was specified as default. - unless controller = controller(params, @defaults.key?(:controller)) + controller = controller(params, @raise_on_name_error) do return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], req.env) + dispatch(controller, params[:action], req) end def prepare_params!(params) @@ -53,24 +52,26 @@ module ActionDispatch # segment, as in :controller(/:action), we should simply return nil and # delegate the control back to Rack cascade. Besides, if this is not a default # controller, it means we should respect the @scope[:module] parameter. - def controller(params, default_controller=true) - if params && params.key?(:controller) - controller_param = params[:controller] - controller_reference(controller_param) - end + def controller(params, raise_on_name_error=true) + controller_reference params.fetch(:controller) { yield } rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if default_controller + raise ActionController::RoutingError, e.message, e.backtrace if raise_on_name_error + yield end - private + protected + + attr_reader :controller_class_names def controller_reference(controller_param) - const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" + const_name = controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" ActiveSupport::Dependencies.constantize(const_name) end - def dispatch(controller, action, env) - controller.action(action).call(env) + private + + def dispatch(controller, action, req) + controller.action(action).call(req.env) end def normalize_controller!(params) @@ -267,7 +268,7 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - inner_options.each do |key, _| + inner_options.each_key do |key| path_params.delete(key) end @@ -313,7 +314,7 @@ module ActionDispatch attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names - attr_accessor :default_url_options + attr_accessor :default_url_options, :dispatcher_class attr_reader :env_key alias :routes :set @@ -356,6 +357,7 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set + @dispatcher_class = Routing::RouteSet::Dispatcher end def relative_url_root @@ -413,8 +415,8 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def dispatcher(defaults) - Routing::RouteSet::Dispatcher.new(defaults) + def dispatcher(raise_on_name_error) + dispatcher_class.new(raise_on_name_error) end module MountedHelpers @@ -587,10 +589,8 @@ module ActionDispatch PARAMETERIZE = lambda do |name, value| if name == :controller value - elsif value.is_a?(Array) - value.map(&:to_param).join('/') - elsif param = value.to_param - param + else + value.to_param end end @@ -815,12 +815,12 @@ module ActionDispatch if app.matches?(req) && app.dispatcher? dispatcher = app.app - if dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params - else + dispatcher.controller(params, false) do raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" end + + dispatcher.prepare_params!(params) + return params end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index d0e3ea818e..54e24ed6bf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -86,8 +86,8 @@ module ActionDispatch end # Load routes.rb if it hasn't been loaded. - generated_path, extra_keys = @routes.generate_extras(options, defaults) - found_extras = options.reject { |k, _| ! extra_keys.include? k } + generated_path, query_string_keys = @routes.generate_extras(options, defaults) + found_extras = options.reject { |k, _| ! query_string_keys.include? k } msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 0298962409..0cdc6d4e77 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -325,7 +325,11 @@ module ActionDispatch if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme - host! "#{location.host}:#{location.port}" if location.host + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end path = location.query ? "#{location.path}?#{location.query}" : location.path end @@ -374,7 +378,7 @@ module ActionDispatch @html_document = nil @url_options = nil - @controller = session.last_request.env['action_controller.instance'] + @controller = @request.controller_instance response.status end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 494644cd46..c28d701b48 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -19,7 +19,7 @@ module ActionDispatch end def cookies - @cookie_jar ||= Cookies::CookieJar.build(@request.env, @request.host, @request.ssl?, @request.cookies) + @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies) end def redirect_to_url diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index cc610b6d75..60e2cea8a2 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -63,6 +63,10 @@ FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') SharedTestRoutes = ActionDispatch::Routing::RouteSet.new +SharedTestRoutes.draw do + get ':controller(/:action)' +end + module ActionDispatch module SharedRoutes def before_setup @@ -70,35 +74,10 @@ module ActionDispatch super end end - - # Hold off drawing routes until all the possible controller classes - # have been loaded. - module DrawOnce - class << self - attr_accessor :drew - end - self.drew = false - - def before_setup - super - return if DrawOnce.drew - - SharedTestRoutes.draw do - get ':controller(/:action)' - end - - ActionDispatch::IntegrationTest.app.routes.draw do - get ':controller(/:action)' - end - - DrawOnce.drew = true - end - end end module ActiveSupport class TestCase - include ActionDispatch::DrawOnce if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 parallelize_me! end @@ -119,23 +98,25 @@ class RoutedRackApp end class ActionDispatch::IntegrationTest < ActiveSupport::TestCase - include ActionDispatch::SharedRoutes - def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::ParamsParser + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end self.app = build_app + app.routes.draw do + get ':controller(/:action)' + end + # Stub Rails dispatcher so it does not get controller references and # simply return the controller#action as Rack::Body. class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher @@ -149,14 +130,10 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end end - def self.stub_controllers - old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } - yield ActionDispatch::Routing::RouteSet.new - ensure - ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } - ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } + def self.stub_controllers(config = nil) + route_set = ActionDispatch::Routing::RouteSet.new(*[config].compact) + route_set.dispatcher_class = StubDispatcher + yield route_set end def with_routing(&block) diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 64543f0659..b063d769a4 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -329,7 +329,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, :key => SessionKey middleware.use ActionDispatch::Flash - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 0b9be8d671..dc4c32b07e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -755,6 +755,25 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest assert_equal "http://test.com/", @request.env["HTTP_REFERER"] end + def test_ignores_common_ports_in_host + get "http://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + + get "https://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + end + + def test_keeps_uncommon_ports_in_host + get "http://test.com:123" + assert_equal "test.com:123", @request.env["HTTP_HOST"] + + get "http://test.com:443" + assert_equal "test.com:443", @request.env["HTTP_HOST"] + + get "https://test.com:80" + assert_equal "test.com:80", @request.env["HTTP_HOST"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/controller/new_base/metal_test.rb b/actionpack/test/controller/new_base/metal_test.rb deleted file mode 100644 index 537b93387a..0000000000 --- a/actionpack/test/controller/new_base/metal_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'abstract_unit' - -module MetalTest - class MetalMiddleware < ActionController::Middleware - def call(env) - if env["PATH_INFO"] =~ /authed/ - app.call(env) - else - [401, headers, "Not authed!"] - end - end - end - - class Endpoint - def call(env) - [200, {}, "Hello World"] - end - end - - class TestMiddleware < ActiveSupport::TestCase - def setup - @app = Rack::Builder.new do - use MetalTest::MetalMiddleware - run MetalTest::Endpoint.new - end.to_app - end - - test "it can call the next app by using @app" do - env = Rack::MockRequest.env_for("/authed") - response = @app.call(env) - - assert_equal ["Hello World"], response[2] - end - - test "it can return a response using the normal AC::Metal techniques" do - env = Rack::MockRequest.env_for("/") - response = @app.call(env) - - assert_equal ["Not authed!"], response[2] - assert_equal 401, response[0] - end - end -end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 5a279639cc..04d6cc1792 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1128,14 +1128,14 @@ class ResourcesTest < ActionController::TestCase end def assert_restful_routes_for(controller_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end new_action = @routes.resources_path_names[:new] || "new" @@ -1154,7 +1154,7 @@ class ResourcesTest < ActionController::TestCase edit_member_path = "#{member_path}/#{edit_action}" formatted_edit_member_path = "#{member_path}/#{edit_action}.xml" - with_options(options[:options]) do |controller| + with_options(route_options) do |controller| controller.assert_routing collection_path, :action => 'index' controller.assert_routing new_path, :action => 'new' controller.assert_routing "#{collection_path}.xml", :action => 'index', :format => 'xml' @@ -1168,23 +1168,23 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_member_path, :action => 'edit', :id => '1', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'index'), :path => collection_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => collection_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'index'), :path => collection_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => collection_path, :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1'), :path => member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1'), :path => edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1'), :path => member_path, :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1'), :path => member_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post) assert_recognizes(options[:shallow_options].merge(:action => 'show', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'edit', :id => '1', :format => 'xml'), :path => formatted_edit_member_path, :method => :get) assert_recognizes(options[:shallow_options].merge(:action => 'update', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :put) assert_recognizes(options[:shallow_options].merge(:action => 'destroy', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end # test named routes like foo_path and foos_path map to the correct options. @@ -1195,20 +1195,20 @@ class ResourcesTest < ActionController::TestCase end singular_name ||= controller_name.to_s.singularize - options[:options] ||= {} - options[:options][:controller] = options[:controller] || controller_name.to_s + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s if options[:shallow] options[:shallow_options] ||= {} - options[:shallow_options][:controller] = options[:options][:controller] + options[:shallow_options][:controller] = route_options[:controller] else - options[:shallow_options] = options[:options] + options[:shallow_options] = route_options end - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + @controller = "#{route_options[:controller].camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - get :index, params: options[:options] - options[:options].delete :action + get :index, params: route_options + route_options.delete :action path = "#{options[:as] || controller_name}" shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}" @@ -1223,29 +1223,29 @@ class ResourcesTest < ActionController::TestCase edit_action = options[:path_names][:edit] || "edit" end - assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", options[:options] - assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options + assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(:format => 'xml') assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1') assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(:id => '1', :format => 'xml') - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_routes_for(singleton_name, options = {}) - options[:options] ||= {} - options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" new_path = "#{full_path}/new" edit_path = "#{full_path}/edit" formatted_edit_path = "#{full_path}/edit.xml" - with_options options[:options] do |controller| + with_options route_options do |controller| controller.assert_routing full_path, :action => 'show' controller.assert_routing new_path, :action => 'new' controller.assert_routing edit_path, :action => 'edit' @@ -1254,40 +1254,41 @@ class ResourcesTest < ActionController::TestCase controller.assert_routing formatted_edit_path, :action => 'edit', :format => 'xml' end - assert_recognizes(options[:options].merge(:action => 'show'), :path => full_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit'), :path => edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create'), :path => full_path, :method => :post) - assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete) + assert_recognizes(route_options.merge(:action => 'show'), :path => full_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'new'), :path => new_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'edit'), :path => edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create'), :path => full_path, :method => :post) + assert_recognizes(route_options.merge(:action => 'update'), :path => full_path, :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy'), :path => full_path, :method => :delete) - assert_recognizes(options[:options].merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) - assert_recognizes(options[:options].merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) - assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) - assert_recognizes(options[:options].merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) - assert_recognizes(options[:options].merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) + assert_recognizes(route_options.merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get) + assert_recognizes(route_options.merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get) + assert_recognizes(route_options.merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post) + assert_recognizes(route_options.merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put) + assert_recognizes(route_options.merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete) - yield options[:options] if block_given? + yield route_options if block_given? end def assert_singleton_named_routes_for(singleton_name, options = {}) - (options[:options] ||= {})[:controller] ||= singleton_name.to_s.pluralize - @controller = "#{options[:options][:controller].camelize}Controller".constantize.new + route_options = (options[:options] ||= {}).dup + controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize + @controller = "#{controller_name.camelize}Controller".constantize.new @controller.singleton_class.include(@routes.url_helpers) - get :show, params: options[:options] - options[:options].delete :action + get :show, params: route_options + route_options.delete :action full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" name_prefix = options[:name_prefix] - assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') - assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') - assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", options[:options] - assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", options[:options].merge(:format => 'xml') + assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') + assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(:format => 'xml') end def assert_named_route(expected, route, options) diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index 7a29a7ff97..3dac582407 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -19,15 +19,13 @@ module ActionDispatch setup do Rails.stubs(:root).returns(Pathname.new('.')) - cleaner = ActiveSupport::BacktraceCleaner.new - cleaner.add_silencer { |line| line !~ /^lib/ } - - @environment = { 'action_dispatch.backtrace_cleaner' => cleaner } + @cleaner = ActiveSupport::BacktraceCleaner.new + @cleaner.add_silencer { |line| line !~ /^lib/ } end test '#source_extracts fetches source fragments for every backtrace entry' do exception = TestError.new("lib/file.rb:42:in `index'") - wrapper = ExceptionWrapper.new({}, exception) + wrapper = ExceptionWrapper.new(nil, exception) wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo') @@ -37,7 +35,7 @@ module ActionDispatch test '#source_extracts works with Windows paths' do exc = TestError.new("c:/path/to/rails/app/controller.rb:27:in 'index':") - wrapper = ExceptionWrapper.new({}, exc) + wrapper = ExceptionWrapper.new(nil, exc) wrapper.expects(:source_fragment).with('c:/path/to/rails/app/controller.rb', 27).returns('nothing') assert_equal [ code: 'nothing', line_number: 27 ], wrapper.source_extracts @@ -46,7 +44,7 @@ module ActionDispatch test '#source_extracts works with non standard backtrace' do exc = TestError.new('invalid') - wrapper = ExceptionWrapper.new({}, exc) + wrapper = ExceptionWrapper.new(nil, exc) wrapper.expects(:source_fragment).with('invalid', 0).returns('nothing') assert_equal [ code: 'nothing', line_number: 0 ], wrapper.source_extracts @@ -54,14 +52,14 @@ module ActionDispatch test '#application_trace returns traces only from the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace end test '#application_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.application_trace assert_equal [], nil_cleaner_wrapper.application_trace @@ -69,14 +67,14 @@ module ActionDispatch test '#framework_trace returns traces outside the application' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal caller, wrapper.framework_trace end test '#framework_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.framework_trace assert_equal [], nil_cleaner_wrapper.framework_trace @@ -84,14 +82,14 @@ module ActionDispatch test '#full_trace returns application and framework traces' do exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal exception.backtrace, wrapper.full_trace end test '#full_trace cannot be nil' do - nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new) - nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new) + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) assert_equal [], nil_backtrace_wrapper.full_trace assert_equal [], nil_cleaner_wrapper.full_trace @@ -99,7 +97,7 @@ module ActionDispatch test '#traces returns every trace by category enumerated with an index' do exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'") - wrapper = ExceptionWrapper.new(@environment, exception) + wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal({ 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ], diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 889f9a4736..aed1d914f9 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -19,6 +19,10 @@ module ActionDispatch ActionDispatch::Request end + def dispatcher_class + RouteSet::Dispatcher + end + def add_route(*args) routes << args end diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb deleted file mode 100644 index 9607f026db..0000000000 --- a/actionpack/test/dispatch/middleware_stack/middleware_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'abstract_unit' -require 'action_dispatch/middleware/stack' - -module ActionDispatch - class MiddlewareStack - class MiddlewareTest < ActiveSupport::TestCase - class Omg; end - - { - 'concrete' => Omg, - 'anonymous' => Class.new - }.each do |name, klass| - - define_method("test_#{name}_klass") do - mw = Middleware.new klass - assert_equal klass, mw.klass - end - - define_method("test_#{name}_==") do - mw1 = Middleware.new klass - mw2 = Middleware.new klass - assert_equal mw1, mw2 - end - - end - - def test_string_class - mw = Middleware.new Omg.name - assert_equal Omg, mw.klass - end - - def test_double_equal_works_with_classes - k = Class.new - mw = Middleware.new k - assert_operator mw, :==, k - - result = mw != Class.new - assert result, 'middleware should not equal other anon class' - end - - def test_double_equal_works_with_strings - mw = Middleware.new Omg - assert_operator mw, :==, Omg.name - end - - def test_double_equal_normalizes_strings - mw = Middleware.new Omg - assert_operator mw, :==, "::#{Omg.name}" - end - - def test_middleware_loads_classnames_from_cache - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg.name) - - fake_cache = { mw.name => Omg } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - - fake_cache[mw.name] = Middleware - assert_equal Middleware, mw.klass - end - - def test_middleware_always_returns_class - mw = Class.new(Middleware) { - attr_accessor :classcache - }.new(Omg) - - fake_cache = { mw.name => Middleware } - mw.classcache = fake_cache - - assert_equal Omg, mw.klass - end - end - end -end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb index 948a690979..33aa616474 100644 --- a/actionpack/test/dispatch/middleware_stack_test.rb +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -4,6 +4,7 @@ class MiddlewareStackTest < ActiveSupport::TestCase class FooMiddleware; end class BarMiddleware; end class BazMiddleware; end + class HiyaMiddleware; end class BlockMiddleware attr_reader :block def initialize(&block) @@ -17,6 +18,20 @@ class MiddlewareStackTest < ActiveSupport::TestCase @stack.use BarMiddleware end + def test_delete_with_string_is_deprecated + assert_deprecated do + assert_difference "@stack.size", -1 do + @stack.delete FooMiddleware.name + end + end + end + + def test_delete_works + assert_difference "@stack.size", -1 do + @stack.delete FooMiddleware + end + end + test "use should push middleware as class onto the stack" do assert_difference "@stack.size" do @stack.use BazMiddleware @@ -25,17 +40,21 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "use should push middleware as a string onto the stack" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware as a symbol onto the stack" do - assert_difference "@stack.size" do - @stack.use :"MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use :"MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass end test "use should push middleware class with arguments onto the stack" do @@ -88,30 +107,28 @@ class MiddlewareStackTest < ActiveSupport::TestCase end test "unshift adds a new middleware at the beginning of the stack" do - @stack.unshift :"MiddlewareStackTest::BazMiddleware" - assert_equal BazMiddleware, @stack.first.klass + assert_deprecated do + @stack.unshift :"MiddlewareStackTest::BazMiddleware" + assert_equal BazMiddleware, @stack.first.klass + end end test "raise an error on invalid index" do assert_raise RuntimeError do - @stack.insert("HiyaMiddleware", BazMiddleware) + @stack.insert(HiyaMiddleware, BazMiddleware) end assert_raise RuntimeError do - @stack.insert_after("HiyaMiddleware", BazMiddleware) + @stack.insert_after(HiyaMiddleware, BazMiddleware) end end test "lazy evaluates middleware class" do - assert_difference "@stack.size" do - @stack.use "MiddlewareStackTest::BazMiddleware" + assert_deprecated do + assert_difference "@stack.size" do + @stack.use "MiddlewareStackTest::BazMiddleware" + end + assert_equal BazMiddleware, @stack.last.klass end - assert_equal BazMiddleware, @stack.last.klass - end - - test "lazy compares so unloaded constants are not loaded" do - @stack.use "UnknownMiddleware" - @stack.use :"MiddlewareStackTest::BazMiddleware" - assert @stack.include?("::MiddlewareStackTest::BazMiddleware") end end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 939a771c65..b36fbd3c76 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -63,6 +63,17 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal 'contents', file.read end + test "parses utf8 filename with percent character" do + params = parse_multipart('utf8_filename') + assert_equal %w(file foo), params.keys.sort + assert_equal 'bar', params['foo'] + + file = params['file'] + assert_equal 'ファイル%名.txt', file.original_filename + assert_equal "text/plain", file.content_type + assert_equal 'contents', file.read + end + test "parses boundary problem file" do params = parse_multipart('boundary_problem_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index b18a9ab647..280b258da6 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -168,12 +168,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_session_singleton_resource_for_api_app - self.class.stub_controllers do |_| - config = ActionDispatch::Routing::RouteSet::Config.new - config.api_only = true - - routes = ActionDispatch::Routing::RouteSet.new(config) + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + self.class.stub_controllers(config) do |routes| routes.draw do resource :session do get :create @@ -550,11 +548,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_projects_for_api_app - self.class.stub_controllers do |_| - config = ActionDispatch::Routing::RouteSet::Config.new - config.api_only = true + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true - routes = ActionDispatch::Routing::RouteSet.new(config) + self.class.stub_controllers(config) do |routes| routes.draw do resources :projects, controller: :project end diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index e6a70358c8..dbb996973d 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -170,7 +170,7 @@ class CacheStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| @cache = ActiveSupport::Cache::MemoryStore.new middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 715eb90566..e432c65c62 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -348,7 +348,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::CookieStore, options - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 6e9107ecbf..3fed9bad4f 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -192,7 +192,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest @app = self.class.build_app(set) do |middleware| middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}" - middleware.delete "ActionDispatch::ShowExceptions" + middleware.delete ActionDispatch::ShowExceptions end yield diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb index d30461a623..59c030176b 100644 --- a/actionpack/test/dispatch/session/test_session_test.rb +++ b/actionpack/test/dispatch/session/test_session_test.rb @@ -40,4 +40,14 @@ class ActionController::TestSessionTest < ActiveSupport::TestCase assert_equal %w(one two), session.keys assert_equal %w(1 2), session.values end + + def test_fetch_returns_default + session = ActionController::TestSession.new(one: '1') + assert_equal('2', session.fetch(:two, '2')) + end + + def test_fetch_returns_block_value + session = ActionController::TestSession.new(one: '1') + assert_equal(2, session.fetch('2') { |key| key.to_i }) + end end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 95971b3a0e..13dec8b618 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -156,7 +156,7 @@ module StaticTests def test_does_not_modify_path_info file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" - env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'} + env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip', "REQUEST_METHOD" => 'POST'} @app.call(env) assert_equal file_name, env['PATH_INFO'] end diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename new file mode 100644 index 0000000000..60738d53b0 --- /dev/null +++ b/actionpack/test/fixtures/multipart/utf8_filename @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%名.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 4635c645d0..35f520ed1c 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -147,13 +147,13 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| - middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") - middleware.use "ActionDispatch::DebugExceptions" - middleware.use "ActionDispatch::Callbacks" - middleware.use "ActionDispatch::ParamsParser" - middleware.use "ActionDispatch::Cookies" - middleware.use "ActionDispatch::Flash" - middleware.use "Rack::Head" + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::ParamsParser + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::Head yield(middleware) if block_given? end end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 1335e3236e..e8ceabaeba 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -15,18 +15,18 @@ module ActiveJob # # === Backends Features # - # | | Async | Queues | Delayed | Priorities | Timeout | Retries | - # |-------------------|-------|--------|-----------|------------|---------|---------| - # | Backburner | Yes | Yes | Yes | Yes | Job | Global | - # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | - # | Qu | Yes | Yes | No | No | No | Global | - # | Que | Yes | Yes | Yes | Job | No | Job | - # | queue_classic | Yes | Yes | No* | No | No | No | - # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | - # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | - # | Sneakers | Yes | Yes | No | Queue | Queue | No | - # | Sucker Punch | Yes | Yes | No | No | No | No | - # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | + # | | Async | Queues | Delayed | Priorities | Timeout | Retries | + # |-------------------|-------|--------|------------|------------|---------|---------| + # | Backburner | Yes | Yes | Yes | Yes | Job | Global | + # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | + # | Qu | Yes | Yes | No | No | No | Global | + # | Que | Yes | Yes | Yes | Job | No | Job | + # | queue_classic | Yes | Yes | Yes* | No | No | No | + # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | + # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | + # | Sneakers | Yes | Yes | No | Queue | Queue | No | + # | Sucker Punch | Yes | Yes | No | No | No | No | + # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | # # ==== Async # @@ -50,9 +50,8 @@ module ActiveJob # N/A: The adapter does not support queueing. # # NOTE: - # queue_classic does not support job scheduling. - # However, you can use the queue_classic-later gem. - # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic supports job scheduling since version 3.1. + # For older versions you can use the queue_classic-later gem. # # ==== Priorities # diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index e23380a7bf..229517774e 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -1,5 +1,6 @@ require 'helper' require 'jobs/gid_job' +require 'jobs/hello_job' require 'models/person' class JobSerializationTest < ActiveSupport::TestCase diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index dddfd940bb..f25c901ecc 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -3,6 +3,10 @@ *Jay Elaraj* +* Remove `ActiveModel::Serializers::Xml` from core. + + *Zachary Scott* + * Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and `ActiveModel::Dirty#[attr_name]_previous_change` to improve access to recorded changes after the model has been saved. diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index d954467387..20414c1d61 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -155,7 +155,7 @@ behavior out of the box: * Making objects serializable <tt>ActiveModel::Serialization</tt> provides a standard interface for your object - to provide +to_json+ or +to_xml+ serialization. + to provide +to_json+ serialization. class SerialPerson include ActiveModel::Serialization @@ -177,13 +177,6 @@ behavior out of the box: s = SerialPerson.new s.to_json # => "{\"name\":null}" - class SerialPerson - include ActiveModel::Serializers::Xml - end - - s = SerialPerson.new - s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... - {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 8aa1b6f664..4e1b3f7495 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -58,7 +58,6 @@ module ActiveModel eager_autoload do autoload :JSON - autoload :Xml end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index f95849eb84..58d68d9620 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -40,7 +40,6 @@ module ActiveModel # # class Person # include ActiveModel::Serializers::JSON - # include ActiveModel::Serializers::Xml # # attr_accessor :name # @@ -55,13 +54,11 @@ module ActiveModel # person.serializable_hash # => {"name"=>nil} # person.as_json # => {"name"=>nil} # person.to_json # => "{\"name\":null}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # person.as_json # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... # # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and # <tt>:include</tt>. The following are all valid examples: diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb deleted file mode 100644 index e33c766627..0000000000 --- a/activemodel/lib/active_model/serializers/xml.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/time/acts_like' - -module ActiveModel - module Serializers - # == \Active \Model XML Serializer - module Xml - extend ActiveSupport::Concern - include ActiveModel::Serialization - - included do - extend ActiveModel::Naming - end - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - - if value.acts_like?(:time) && value.respond_to?(:in_time_zone) - value = value.in_time_zone - end - - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << { xmlns: options[:namespace] } if options[:namespace] - args << { type: options[:type] } if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - [:skip_types, :dasherize, :camelize].each do |key| - merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil? - end - - if records.respond_to?(:to_ary) - records = records.to_ary - - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : { type: "array" } - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = { type: record_class } - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - - unless records.class.to_s.underscore == association.to_s - merged_options[:type] = records.class.name - end - - records.to_xml merged_options - end - end - - def add_procs - if procs = options.delete(:procs) - Array(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the - # model's attributes. - # - # user = User.find(1) - # user.to_xml - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the - # attributes included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt> - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - # Sets the model +attributes+ from an XML string. Returns +self+. - # - # class Person - # include ActiveModel::Serializers::Xml - # - # attr_accessor :name, :age, :awesome - # - # def attributes=(hash) - # hash.each do |key, value| - # instance_variable_set("@#{key}", value) - # end - # end - # - # def attributes - # instance_values - # end - # end - # - # xml = { name: 'bob', age: 22, awesome:true }.to_xml - # person = Person.new - # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob"> - # person.name # => "bob" - # person.age # => 22 - # person.awesome # => true - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb deleted file mode 100644 index 37faf6cef8..0000000000 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ /dev/null @@ -1,251 +0,0 @@ -require 'cases/helper' -require 'models/contact' -require 'active_support/core_ext/object/instance_variables' -require 'ostruct' -require 'yaml' - -module Admin - class Contact < ::Contact - end -end - -class Customer < Struct.new(:name) -end - -class Address - include ActiveModel::Serializers::Xml - - attr_accessor :street, :city, :state, :zip, :apt_number - - def attributes - instance_values - end -end - -class SerializableContact < Contact - def serializable_hash(options={}) - super(options.merge(only: [:name, :age])) - end -end - -class XmlSerializationTest < ActiveModel::TestCase - def setup - @contact = Contact.new - @contact.name = 'aaron stack' - @contact.age = 25 - @contact.created_at = Time.utc(2006, 8, 1) - @contact.awesome = false - customer = Customer.new - customer.name = "John" - @contact.preferences = customer - @contact.address = Address.new - @contact.address.city = "Springfield" - @contact.address.apt_number = 35 - @contact.friends = [Contact.new, Contact.new] - @contact.contact = SerializableContact.new - end - - test "should serialize default root" do - xml = @contact.to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize namespaced root" do - xml = Admin::Contact.new(@contact.attributes).to_xml - assert_match %r{^<contact>}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize default root with namespace" do - xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml - assert_match %r{</contact>$}, xml - end - - test "should serialize custom root" do - xml = @contact.to_xml root: 'xml_contact' - assert_match %r{^<xml-contact>}, xml - assert_match %r{</xml-contact>$}, xml - end - - test "should allow undasherized tags" do - xml = @contact.to_xml root: 'xml_contact', dasherize: false - assert_match %r{^<xml_contact>}, xml - assert_match %r{</xml_contact>$}, xml - assert_match %r{<created_at}, xml - end - - test "should allow camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: true - assert_match %r{^<XmlContact>}, xml - assert_match %r{</XmlContact>$}, xml - assert_match %r{<CreatedAt}, xml - end - - test "should allow lower-camelized tags" do - xml = @contact.to_xml root: 'xml_contact', camelize: :lower - assert_match %r{^<xmlContact>}, xml - assert_match %r{</xmlContact>$}, xml - assert_match %r{<createdAt}, xml - end - - test "should use serializable hash" do - @contact = SerializableContact.new - @contact.name = 'aaron stack' - @contact.age = 25 - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<age type="integer">25</age>}, xml - assert_no_match %r{<awesome>}, xml - end - - test "should allow skipped types" do - xml = @contact.to_xml skip_types: true - assert_match %r{<age>25</age>}, xml - end - - test "should include yielded additions" do - xml_output = @contact.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, xml_output - end - - test "should serialize string" do - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - test "should serialize nil" do - assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms) - end - - test "should serialize integer" do - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - test "should serialize datetime" do - assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - test "should serialize boolean" do - assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml - end - - test "should serialize array" do - assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social) - end - - test "should serialize hash" do - assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network) - end - - test "should serialize yaml" do - assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml - end - - test "should call proc on object" do - proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<nationality>unknown</nationality>}, xml - end - - test "should supply serializable to second proc argument" do - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = @contact.to_xml(procs: [ proc ]) - assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml - end - - test "should serialize string correctly when type passed" do - xml = @contact.to_xml type: 'Contact' - assert_match %r{<contact type="Contact">}, xml - assert_match %r{<name>aaron stack</name>}, xml - end - - test "include option with singular association" do - xml = @contact.to_xml include: :address, indent: 0 - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - end - - test "include option with plural association" do - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - class FriendList - def initialize(friends) - @friends = friends - end - - def to_ary - @friends - end - end - - test "include option with ary" do - @contact.friends = FriendList.new(@contact.friends) - xml = @contact.to_xml include: :friends, indent: 0 - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "multiple includes" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ] - assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true)) - assert_match %r{<friends type="array">}, xml - assert_match %r{<friend type="Contact">}, xml - end - - test "include with options" do - xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } } - assert xml.include?(%(><address><city>Springfield</city></address>)) - end - - test "propagates skip_types option to included associations" do - xml = @contact.to_xml include: :friends, indent: 0, skip_types: true - assert_match %r{<friends>}, xml - assert_match %r{<friend>}, xml - end - - test "propagates skip-types option to included associations and attributes" do - xml = @contact.to_xml skip_types: true, include: :address, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number>}, xml - end - - test "propagates camelize option to included associations and attributes" do - xml = @contact.to_xml camelize: true, include: :address, indent: 0 - assert_match %r{<Address>}, xml - assert_match %r{<AptNumber type="integer">}, xml - end - - test "propagates dasherize option to included associations and attributes" do - xml = @contact.to_xml dasherize: false, include: :address, indent: 0 - assert_match %r{<apt_number type="integer">}, xml - end - - test "don't propagate skip_types if skip_types is defined at the included association level" do - xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate camelize if camelize is defined at the included association level" do - xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "don't propagate dasherize if dasherize is defined at the included association level" do - xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0 - assert_match %r{<address>}, xml - assert_match %r{<apt-number type="integer">}, xml - end - - test "association with sti" do - xml = @contact.to_xml(include: :contact) - assert xml.include?(%(<contact type="SerializableContact">)) - end -end diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index bcfd267a34..113ab0bc1f 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -4,7 +4,6 @@ class Contact include ActiveModel::Validations include ActiveModel::Serializers::JSON - include ActiveModel::Serializers::Xml attr_accessor :id, :name, :age, :created_at, :awesome, :preferences attr_accessor :address, :friends, :contact diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 5f273d53f2..c1ea8a3252 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,37 @@ +* Add `ActiveRecord::Relation#in_batches` to work with records and relations + in batches. + + Available options are `of` (batch size), `load`, `begin_at`, and `end_at`. + + Examples: + + Person.in_batches.each_record(&:party_all_night!) + Person.in_batches.update_all(awesome: true) + Person.in_batches.delete_all + Person.in_batches.each do |relation| + relation.delete_all + sleep 10 # Throttles the delete queries + end + + Closes #20933. + + *Sina Siadat* + +* Added methods for PostgreSQL geometric data types to use in migrations + + Example: + + create_table :foo do |t| + t.line :foo_line + t.lseg :foo_lseg + t.box :foo_box + t.path :foo_path + t.polygon :foo_polygon + t.circle :foo_circle + end + + *Mehmet Emin İNAÇ* + * Add `cache_key` to ActiveRecord::Relation. Example: @@ -311,6 +345,10 @@ *Ryuta Kamizono* +* Remove `ActiveRecord::Serialization::XmlSerializer` from core. + + *Zachary Scott* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 87576abd92..0fc2b83b71 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -62,8 +62,10 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..b7db57c9fe 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -254,7 +254,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] + @full_version ||= @connection.server_info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4cfda302ea..b7b508d853 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -716,7 +716,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..87a1988f2f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -6,7 +6,7 @@ module ActiveRecord delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all - delegate :find_each, :find_in_batches, to: :all + delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index da6b8447d3..459d6256fa 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,11 +16,11 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::QueryCache - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::ConnectionAdapters::ConnectionManagement config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, @@ -78,7 +78,7 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", + config.app_middleware.insert_after ::ActionDispatch::Callbacks, "ActiveRecord::Migration::CheckPending" end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 3ed04dee3b..e47b7b1ed9 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -667,6 +667,13 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end + protected + + def load_records(records) + @records = records + @loaded = true + end + private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..beb8fa511c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,3 +1,5 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches # Looping through a collection of records from the database @@ -122,24 +124,102 @@ module ActiveRecord end end + in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + yield batch.to_a + end + end + + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # If you do not provide a block to #in_batches, it will return a + # BatchEnumerator which is enumerable. + # + # Person.in_batches.with_index do |relation, batch_index| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # Person.in_batches.each_record(&:party_all_night!) + # + # ==== Options + # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # + # This is especially useful if you want to work with the + # ActiveRecord::Relation object instead of the array of records, or if + # you want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and worker 2 + # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # option on each worker). + # + # # Let's process the next 2000 records + # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # consistent. Therefore the primary key must be orderable, e.g an integer + # or a string. + # + # NOTE: You can't set the limit either, that's used to control the batch + # sizes. + def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + end + if logger && (arel.orders.present? || arel.taken.present?) logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - relation = relation.reorder(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(of) relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + batch_relation = relation + + loop do + if load + records = batch_relation.to_a + ids = records.map(&:id) + yielded_relation = self.where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = self.where(primary_key => ids) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + break if ids.empty? - yield records + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - break if records_size < batch_size + yield yielded_relation - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + break if ids.length < of + batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..153aae9584 --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + @of = of + @relation = relation + @begin_at = begin_at + @end_at = end_at + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| + relation.to_a.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.in_batches.destroy_all('age < 10') + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..23dc6465af 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 21e2ee3b11..2cbee3cd21 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2308,4 +2308,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_instance_of PostWithErrorDestroying, error.record end + + def test_ids_reader_memoization + car = Car.create!(name: 'Tofaş') + bulb = Bulb.create!(car: car) + + assert_equal [bulb.id], car.bulb_ids + assert_no_queries { car.bulb_ids } + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 0791dde1f2..48dc8135ab 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_raise_if_select_is_set_without_id - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Post.select(:title).find_each(batch_size: 1) { |post| flunk "should not call this block" } @@ -199,7 +199,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_return_an_enumerator enum = nil - assert_queries(0) do + assert_no_queries do enum = Post.find_in_batches(:batch_size => 1) end assert_queries(4) do @@ -210,6 +210,234 @@ class EachTest < ActiveRecord::TestCase end end + def test_in_batches_should_not_execute_any_query + assert_no_queries do + assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2) + end + end + + def test_in_batches_should_yield_relation_if_block_given + assert_queries(6) do + Post.in_batches(of: 2) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_be_enumerable_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_each_record_should_yield_record_if_block_is_given + assert_queries(6) do + Post.in_batches(of: 2).each_record do |post| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_return_enumerator_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_be_ordered_by_id + ids = Post.order('id ASC').pluck(:id) + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert_equal ids[i], post.id + end + end + end + + def test_in_batches_update_all_affect_all_records + assert_queries(6 + 6) do # 6 selects, 6 updates + Post.in_batches(of: 2).update_all(title: "updated-title") + end + assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count + end + + def test_in_batches_delete_all_should_not_delete_records_in_other_batches + not_deleted_count = Post.where('id <= 2').count + Post.where('id > 2').in_batches(of: 2).delete_all + assert_equal 0, Post.where('id > 2').count + assert_equal not_deleted_count, Post.count + end + + def test_in_batches_should_not_be_loaded + Post.in_batches(of: 1) do |relation| + assert_not relation.loaded? + end + + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + + def test_in_batches_should_be_loaded + Post.in_batches(of: 1, load: true) do |relation| + assert relation.loaded? + end + end + + def test_in_batches_if_not_loaded_executes_more_queries + assert_queries(@total + 1) do + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + end + + def test_in_batches_should_return_relations + assert_queries(@total + 1) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_start_from_the_start_option + post = Post.order('id ASC').where('id >= ?', 2).first + assert_queries(2) do + relation = Post.in_batches(of: 1, begin_at: 2).first + assert_equal post, relation.first + end + end + + def test_in_batches_should_end_at_the_end_option + post = Post.order('id DESC').where('id <= ?', 5).first + assert_queries(7) do + relation = Post.in_batches(of: 1, end_at: 5, load: true).reverse_each.first + assert_equal post, relation.last + end + end + + def test_in_batches_shouldnt_execute_query_unless_needed + assert_queries(2) do + Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + + assert_queries(1) do + Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + end + + def test_in_batches_should_quote_batch_order + c = Post.connection + assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified + not_a_post = "not a post" + def not_a_post.id + raise StandardError.new("not_a_post had #id called on it") + end + + assert_nothing_raised do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + + relation = [not_a_post] * relation.count + end + end + end + + def test_in_batches_should_not_ignore_default_scope_without_order_statements + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.in_batches do |relation| + posts.concat(relation) + end + assert_equal special_posts_ids, posts.map(&:id) + end + + def test_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.in_batches({ of: 42, begin_at: 1 }.freeze){} + end + end + + def test_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.in_batches(of: 1, begin_at: start_nick) do |relation| + subscribers.concat(relation) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + assert_queries(Subscriber.count + 1) do + Subscriber.in_batches(of: 1, load: true) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Subscriber, relation.first + end + end + end + + def test_in_batches_should_return_an_enumerator + enum = nil + assert_no_queries do + enum = Post.in_batches(of: 1) + end + assert_queries(4) do + enum.first(4) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_relations_should_not_overlap_with_each_other + seen_posts = [] + Post.in_batches(of: 2, load: true) do |relation| + relation.to_a.each do |post| + assert_not seen_posts.include?(post) + seen_posts << post + end + end + end + + def test_in_batches_relations_with_condition_should_not_overlap_with_each_other + seen_posts = [] + author_id = Post.first.author_id + posts_by_author = Post.where(author_id: author_id) + Post.in_batches(of: 2) do |batch| + seen_posts += batch.where(author_id: author_id) + end + + assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort + end + + def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches + Post.update_all(author_id: 0) + person = Post.last + person.update_attributes(author_id: 1) + + Post.in_batches(of: 2) do |batch| + batch.where('author_id >= 1').update_all('author_id = author_id + 1') + end + assert_equal 2, person.reload.author_id # incremented only once + end + def test_find_in_batches_start_deprecated assert_deprecated do assert_queries(@total) do diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb new file mode 100644 index 0000000000..e4772905bb --- /dev/null +++ b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + if current_adapter?(:PostgreSQLAdapter) + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + end + + private + def assert_column_exists(column_name) + columns = connection.columns(table_name) + assert columns.map(&:name).include?(column_name.to_s) + end + + def assert_type_correct(column_name, type) + columns = connection.columns(table_name) + column = columns.select{ |c| c.name == column_name.to_s }.first + assert_equal type.to_s, column.sql_type + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index f35e1f5098..fefba5b0fd 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -741,10 +741,10 @@ module ActiveSupport # callback chain, preventing following before and around callbacks from # being called and the event from being triggered. # This should be a lambda to be executed. - # The current object and the return result of the callback will be called - # with the lambda. + # The current object and the result lambda of the callback will be provided + # to the terminator lambda. # - # define_callbacks :validate, terminator: ->(target, result) { result == false } + # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false } # # In this example, if any before validate callbacks returns +false+, # any successive before and around callback is not executed. diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index d4e6b5a1ac..a77da573fe 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -206,7 +206,7 @@ class Module # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) - mattr_writer(*syms, &blk) + mattr_writer(*syms) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 8ac1820776..42560f3515 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -49,7 +49,7 @@ module ActiveSupport end def uncountable?(str) - @regex_array.detect {|regex| regex.match(str) } + @regex_array.any? { |regex| regex === str } end private diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 4de7fa4e4b..b6c991a287 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -338,7 +338,7 @@ module ActiveSupport end # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each do |k,_| + @boundary.each_key do |k| @boundary[k].instance_eval do def ===(other) detect { |i| i === other } ? true : false diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 48f3cc579f..128c5e3d1a 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -76,4 +76,10 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal 'default_reader_value', @module.defr assert_equal 'default_writer_value', @module.class_variable_get('@@defw') end + + def test_should_not_invoke_default_value_block_multiple_times + count = 0 + @module.cattr_accessor(:defcount){ count += 1 } + assert_equal 1, count + end end diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index c31b50fcfc..c39cd34e9a 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -326,7 +326,7 @@ key. The list of emails can be an array of email addresses or a single string with the addresses separated by commas. ```ruby -class AdminMailer < ActionMailer::Base +class AdminMailer < ApplicationMailer default to: Proc.new { Admin.pluck(:email) }, from: 'notification@example.com' @@ -759,6 +759,9 @@ config.action_mailer.smtp_settings = { authentication: 'plain', enable_starttls_auto: true } ``` +Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. +You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or +use another ESP to send email by replacing 'smpt.gmail.com' above with the address of your provider. Mailer Testing -------------- diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 4b2bfaee2f..97180a8414 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -319,9 +319,8 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides two serializers `ActiveModel::Serializers::JSON` and -`ActiveModel::Serializers::Xml`. Both of these modules automatically include -the `ActiveModel::Serialization`. +Rails provides a `ActiveModel::Serializers::JSON` serializer. +This module automatically include the `ActiveModel::Serialization`. ##### ActiveModel::Serializers::JSON @@ -379,62 +378,6 @@ person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob"> person.name # => "Bob" ``` -##### ActiveModel::Serializers::Xml - -To use the `ActiveModel::Serializers::Xml` you only need to change from -`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`. - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes - {'name' => nil} - end -end -``` - -With the `to_xml` you have an XML representing the model. - -```ruby -person = Person.new -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n" -person.name = "Bob" -person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n" -``` - -From an XML string you define the attributes of the model. -You need to have the `attributes=` method defined on your class: - -```ruby -class Person - include ActiveModel::Serializers::Xml - - attr_accessor :name - - def attributes=(hash) - hash.each do |key, value| - send("#{key}=", value) - end - end - - def attributes - {'name' => nil} - end -end -``` - -Now it is possible to create an instance of person and set the attributes using `from_xml`. - -```ruby -xml = { name: 'Bob' }.to_xml -person = Person.new -person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob"> -person.name # => "Bob" -``` - ### Translation `ActiveModel::Translation` provides integration between your object and the Rails diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 980dfe6953..4e5902fb3d 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -1004,7 +1004,10 @@ such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -Some people use migrations to add data to the database: +The main purpose of Rails' migration feature is to issue commands that modify the +schema using a consistent process. Migrations can also be used +to add or modify data. This is useful in an existing database that can't be destroyed +and recreated, such as a production database. ```ruby class AddInitialProducts < ActiveRecord::Migration @@ -1020,9 +1023,11 @@ class AddInitialProducts < ActiveRecord::Migration end ``` -However, Rails has a 'seeds' feature that should be used for seeding a database -with initial data. It's a really simple feature: just fill up `db/seeds.rb` -with some Ruby code, and run `rake db:seed`: +To add initial data after a database is created, Rails has a built-in +'seeds' feature that makes the process quick and easy. This is especially +useful when reloading the database frequently in development and test environments. +It's easy to get started with this feature: just fill up `db/seeds.rb` with some +Ruby code, and run `rake db:seed`: ```ruby 5.times do |i| diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index fe112a4708..9d495dfacb 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -252,7 +252,7 @@ extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| - t.column :identifier, :uuid + t.uuid :identifier end # app/models/revision.rb diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index e49abc41f4..cd44c685ba 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -244,14 +244,6 @@ INFO. The adapters will add their own data as well. } ``` -### identity.active_record - -| Key | Value | -| ---------------- | ----------------------------------------- | -| `:line` | Primary Key of object in the identity map | -| `:name` | Record's class | -| `:connection_id` | `self.object_id` | - ### instantiation.active_record | Key | Value | diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 4a610e8458..fc7dd3a6c6 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -403,13 +403,13 @@ When using the asset pipeline, paths to assets must be re-written and underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet. -* `image-url("rails.png")` becomes `url(/assets/rails.png)` -* `image-path("rails.png")` becomes `"/assets/rails.png"`. +* `url(/assets/rails.png)` becomes `image-url("rails.png")` +* `"/assets/rails.png"` becomes `image-path("rails.png")` . The more generic form can also be used: -* `asset-url("rails.png")` becomes `url(/assets/rails.png)` -* `asset-path("rails.png")` becomes `"/assets/rails.png"` +* `url(/assets/rails.png)` becomes `asset-url("rails.png")` +* `"/assets/rails.png"` becomes `asset-path("rails.png")` #### JavaScript/CoffeeScript and ERB diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index dc1df8f229..44434c164b 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -502,7 +502,7 @@ current context: (byebug) instance_variables [:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name, + :@_response, :@_prefixes, :@_lookup_context, :@_action_name, :@_response_body, :@marked_for_same_origin_verification, :@_config] ``` @@ -533,7 +533,7 @@ And then ask again for the instance_variables: ``` (byebug) instance_variables [:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name, + :@_response, :@_prefixes, :@_lookup_context, :@_action_name, :@_response_body, :@marked_for_same_origin_verification, :@_config, :@articles] ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index dbbedc49ab..d51a27812a 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -166,7 +166,7 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | |app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.| +|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.| |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| diff --git a/guides/source/routing.md b/guides/source/routing.md index cf828462ce..732932b26e 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -83,7 +83,9 @@ Rails would dispatch that request to the `destroy` method on the `photos` contro ### CRUD, Verbs, and Actions -In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as: +In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to +controller actions. By convention, each action also maps to a specific CRUD +operation in a database. A single entry in the routing file, such as: ```ruby resources :photos @@ -795,7 +797,11 @@ get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` -Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. +Please note that default redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. You can use the `:status` option to change the response status: + +```ruby +get '/stories/:name', to: redirect('/articles/%{name}', status: 302) +``` In all of these cases, if you don't provide the leading host (`http://www.example.com`), Rails will take those details from the current request. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 17309d4b47..743241d7a0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -75,7 +75,7 @@ warning by adding the following configuration to your `config/application.rb`: See [#17227](https://github.com/rails/rails/pull/17227) for more details. -### ActiveJob jobs now inherent from ApplicationJob by default +### ActiveJob jobs now inherit from ApplicationJob by default In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this behavior has changed to now inherit from `ApplicationJob`. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 30449aa670..0f1aab9839 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,26 @@ +* Add `bin/update` script to update development environment automatically. + + *Mehmet Emin İNAÇ* + +* Fix STATS_DIRECTORIES already defined warning when running rake from within + the top level directory of an engine that has a test app. + + Fixes #20510 + + *Ersin Akinci* + +* Make enabling or disabling caching in development mode possible with + rake dev:cache. + + Running rake dev:cache will create or remove tmp/caching-dev.txt. When this + file exists config.action_controller.perform_caching will be set to true in + config/environments/development.rb. + + Additionally, a server can be started with either --dev-caching or + --no-dev-caching included to toggle caching on startup. + + *Jussi Mertanen*, *Chuck Callebs* + * Add a `--api` option in order to generate plugins that can be added inside an API application. diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 0f4d932749..9baf8aa742 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -63,7 +63,7 @@ INFO Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store) if Rails.cache.respond_to?(:middleware) - config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware) + config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware) end end end diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index 9a29ec21cf..618a09a5b3 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -6,7 +6,7 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: def require_local! unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden + render html: '<p>For security purposes, this information is only available to local requests.</p>'.html_safe, status: :forbidden end end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index d1e445ac70..c8fb58ab05 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -34,6 +34,9 @@ module Rails opts.on("-P", "--pid=pid", String, "Specifies the PID file.", "Default: tmp/pids/server.pid") { |v| options[:pid] = v } + opts.on("-C", "--[no-]dev-caching", + "Specifies whether to perform caching in development.", + "true or false") { |v| options[:caching] = v } opts.separator "" @@ -67,6 +70,7 @@ module Rails print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -86,12 +90,23 @@ module Rails DoNotReverseLookup: true, environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, daemonize: false, + caching: false, pid: File.expand_path("tmp/pids/server.pid") }) end private + def setup_dev_caching + return unless options[:environment] == "development" + + if options[:caching] == false + delete_cache_file + elsif options[:caching] + create_cache_file + end + end + def print_boot_information url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" @@ -101,6 +116,14 @@ module Rails puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end + def create_cache_file + FileUtils.touch("tmp/caching-dev.txt") + end + + def delete_cache_file + FileUtils.rm("tmp/caching-dev.txt") if File.exist?("tmp/caching-dev.txt") + end + def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 463d5f01e4..e90d41cbec 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -519,17 +519,15 @@ module Rails # Define the Rack API for this engine. def call(env) env.merge!(env_config) - if env['SCRIPT_NAME'] - env[routes.env_key] = env['SCRIPT_NAME'].dup - end + req = ActionDispatch::Request.new env + req.routes = routes + req.engine_script_name = req.script_name app.call(env) end # Defines additional Rack env configuration that is added on each call. def env_config - @env_config ||= { - 'action_dispatch.routes' => routes - } + @env_config ||= {} end # Defines the routes for this engine. If a block is given to diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update new file mode 100644 index 0000000000..9830e6b29a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/update @@ -0,0 +1,28 @@ +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system 'bundle check' or system! 'bundle install' + + puts "\n== Updating database ==" + system! 'bin/rake db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rake log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rake restart' +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index ecb5d4170f..e29f0bacaa 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -9,9 +9,19 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. + # Show full error reports. config.consider_all_requests_local = true - config.action_controller.perform_caching = false + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.static_cache_control = "public, max-age=172800" + config.cache_store = :memory_store + else + config.action_controller.perform_caching = false + config.cache_store = :null_store + end + <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb index 95adcc06ff..b1b77629d4 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb @@ -20,6 +20,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb index 896b38bc8f..f302cd6c3d 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb @@ -3,7 +3,10 @@ require 'test_helper' <% module_namespacing do -%> class <%= controller_class_name %>ControllerTest < ActionController::TestCase setup do - @<%= singular_table_name %> = <%= table_name %>(:one) + @<%= singular_table_name %> = <%= fixture_name %>(:one) +<% if mountable_engine? -%> + @routes = Engine.routes +<% end -%> end test "should get index" do diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 41422a656c..78a857e0f1 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -26,7 +26,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: if part = find_part(part_type) response.content_type = part_type - render text: part.respond_to?(:decoded) ? part.decoded : part + render plain: part.respond_to?(:decoded) ? part.decoded : part else raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 2c3d278eca..d3e33584d7 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,6 +3,7 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations + dev framework initializers log @@ -10,8 +11,9 @@ require 'rake' misc restart routes - statistics tmp -).each do |task| +).tap { |arr| + arr << 'statistics' if Rake.application.current_scope.empty? +}.each do |task| load "rails/tasks/#{task}.rake" end diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake new file mode 100644 index 0000000000..e949172d3f --- /dev/null +++ b/railties/lib/rails/tasks/dev.rake @@ -0,0 +1,15 @@ +namespace :dev do + task :cache do + desc 'Toggle development mode caching on/off' + + if File.exist? 'tmp/caching-dev.txt' + File.delete 'tmp/caching-dev.txt' + puts 'Development mode is no longer being cached.' + else + FileUtils.touch 'tmp/caching-dev.txt' + puts 'Development mode is now being cached.' + end + + FileUtils.touch 'tmp/restart.txt' + end +end diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb new file mode 100644 index 0000000000..28d8b22a37 --- /dev/null +++ b/railties/test/application/rake/dev_test.rb @@ -0,0 +1,35 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + module RakeTests + class RakeDevTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + test 'dev:cache creates file and outputs message' do + Dir.chdir(app_path) do + output = `rake dev:cache` + assert File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is now being cached/, output) + end + end + + test 'dev:cache deletes file and outputs message' do + Dir.chdir(app_path) do + output = `rake dev:cache` + output = `rake dev:cache` + assert_not File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is no longer being cached/, output) + end + end + end + end +end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index ba688f1e9e..7a063aeddf 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -44,6 +44,22 @@ class Rails::ServerTest < ActiveSupport::TestCase end end + def test_caching_without_option + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal nil, options[:caching] + end + + def test_caching_with_option + args = ["--dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:caching] + + args = ["--no-dev-caching"] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:caching] + end + def test_log_stdout with_rack_env nil do with_rails_env nil do diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 6f5a815173..1898691806 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -313,6 +313,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/test_helper.rb" do |content| assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) + assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content) end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index a853bfbe21..0c3808a9a0 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -505,4 +505,32 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end + + def test_scaffold_tests_pass_by_default_inside_api_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bundle exec rake db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end end |