diff options
317 files changed, 3402 insertions, 2090 deletions
diff --git a/.travis.yml b/.travis.yml index c648bd2ca7..5d4a9e9c67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,11 @@ script: 'ci/travis.rb' before_install: - gem install bundler - "rm ${BUNDLE_GEMFILE}.lock" + - curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp + - cd /tmp/beanstalkd-1.10/ + - make + - ./beanstalkd & + - cd $TRAVIS_BUILD_DIR before_script: - bundle update cache: bundler @@ -17,7 +17,7 @@ gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master' gem 'coffee-rails', '~> 4.1.0' -gem 'turbolinks' +gem 'turbolinks', github: 'rails/turbolinks', branch: 'master' gem 'arel', github: 'rails/arel', branch: 'master' gem 'mail', github: 'mikel/mail', branch: 'master' @@ -79,7 +79,7 @@ group :test do end platforms :ruby do - gem 'nokogiri', '>= 1.4.5' + gem 'nokogiri', '>= 1.6.7.rc3' # Needed for compiling the ActionDispatch::Journey parser gem 'racc', '>=1.4.6', require: false @@ -90,7 +90,7 @@ platforms :ruby do group :db do gem 'pg', '>= 0.18.0' gem 'mysql', '>= 2.9.0' - gem 'mysql2', '>= 0.4.0', github: 'brianmario/mysql2' + gem 'mysql2', '>= 0.4.0' end end @@ -127,3 +127,4 @@ end # A gem necessary for ActiveRecord tests with IBM DB gem 'ibm_db' if ENV['IBM_DB'] +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock index 0700980bca..563a9f56be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,12 +20,6 @@ GIT redis-namespace GIT - remote: git://github.com/brianmario/mysql2.git - revision: 4d76557499b762d5a62aebb7f6a56510ad221eab - specs: - mysql2 (0.4.0) - -GIT remote: git://github.com/mikel/mail.git revision: 64ef1a12efcdda53fd63e1456c2c564044bf82ce branch: master @@ -35,7 +29,7 @@ GIT GIT remote: git://github.com/rack/rack.git - revision: 4224c028c71c4ccca7cdb3e5a10c51af797a4d4d + revision: c28f271d0c91f45e13bfa8f07bed445ef91f41de branch: master specs: rack (2.0.0.alpha) @@ -43,14 +37,14 @@ GIT GIT remote: git://github.com/rails/arel.git - revision: d5432b4616ff43fbb14540d351eed351e21bb20e + revision: 77ec13b46af2926bfcfc3073685711c874b0d272 branch: master specs: arel (7.0.0.alpha) GIT remote: git://github.com/rails/globalid.git - revision: 8178ff2dc898a8f49dd71f6eb46f2a09712462de + revision: 1d8fca667740570d204fd955a0bd39ac539bac7f branch: master specs: globalid (0.3.6) @@ -58,7 +52,7 @@ GIT GIT remote: git://github.com/rails/jquery-rails.git - revision: 01cb69b17083c8eb8daba13a8daba4907b1c3813 + revision: 04fcfa29b859eef9479f89b6a799d00212902385 branch: master specs: jquery-rails (4.0.5) @@ -68,18 +62,18 @@ GIT GIT remote: git://github.com/rails/sass-rails.git - revision: 805cb17722b8a13ff00dffe20283a6ba2c9a45dc + revision: a3b25261a3d31ed9ff5dd6e841b777790fc86c55 branch: master specs: sass-rails (6.0.0) railties (>= 4.0.0, < 5.0) - sass (~> 3.3) + sass (~> 3.4) sprockets (>= 4.0) sprockets-rails (< 4.0) GIT remote: git://github.com/rails/sprockets-rails.git - revision: e65e088575be4f6b994823b80a277affb0a57a5e + revision: 600f981fe79ff2d4179baf84bd18fac5acb58b5e branch: master specs: sprockets-rails (3.0.0.beta2) @@ -89,11 +83,19 @@ GIT GIT remote: git://github.com/rails/sprockets.git - revision: 408e4ad79df056f6f2199e495782363c22a95c04 + revision: edae5cdfa241b0fb0fcb756b25cd561c3d8b7f29 branch: master specs: sprockets (4.0.0) - rack (~> 2.x) + rack (> 1, < 3) + +GIT + remote: git://github.com/rails/turbolinks.git + revision: 4bb563cd777875d3ad73cf007c26334f2aa8dc37 + branch: master + specs: + turbolinks (3.0.0) + coffee-rails PATH remote: . @@ -128,12 +130,11 @@ PATH activesupport (= 5.0.0.alpha) arel (= 7.0.0.alpha) activesupport (5.0.0.alpha) - concurrent-ruby (~> 0.9.1) + concurrent-ruby (~> 1.0.0.pre2, < 2.0.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) method_source minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) rails (5.0.0.alpha) actionmailer (= 5.0.0.alpha) @@ -157,7 +158,7 @@ GEM remote: https://rubygems.org/ specs: amq-protocol (2.0.0) - backburner (1.0.0) + backburner (1.1.0) beaneater (~> 1.0) dante (> 0.1.5) bcrypt (3.1.10) @@ -166,8 +167,8 @@ GEM beaneater (1.0.0) benchmark-ips (2.3.0) builder (3.2.2) - bunny (2.0.0) - amq-protocol (>= 1.9.2) + bunny (2.2.0) + amq-protocol (>= 2.0.0) byebug (6.0.2) celluloid (0.16.0) timers (~> 4.0.0) @@ -178,7 +179,7 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) - concurrent-ruby (0.9.1) + concurrent-ruby (1.0.0.pre2) connection_pool (2.2.0) dalli (2.7.4) dante (0.2.0) @@ -189,8 +190,8 @@ GEM delayed_job (>= 3.0, < 4.1) erubis (2.7.0) execjs (2.6.0) - hitimes (1.2.2) - hitimes (1.2.2-x86-mingw32) + hitimes (1.2.3) + hitimes (1.2.3-x86-mingw32) i18n (0.7.0) json (1.8.3) kindlerb (0.1.1) @@ -200,8 +201,8 @@ GEM nokogiri (>= 1.5.9) metaclass (0.0.4) method_source (0.8.2) - mime-types (2.6.1) - mini_portile (0.6.2) + mime-types (2.6.2) + mini_portile (0.7.0.rc4) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) @@ -209,15 +210,16 @@ GEM multi_json (1.11.2) mustache (1.0.2) mysql (2.9.1) - 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.2) + mysql2 (0.4.0) + nokogiri (1.6.7.rc3) + mini_portile (~> 0.7.0.rc4) + nokogiri (1.6.7.rc3-x64-mingw32) + mini_portile (~> 0.7.0.rc4) + nokogiri (1.6.7.rc3-x86-mingw32) + mini_portile (~> 0.7.0.rc4) + pg (0.18.3) psych (2.0.15) - que (0.10.0) + que (0.11.2) racc (1.4.12) rack-cache (1.2) rack (>= 0.4) @@ -248,12 +250,12 @@ GEM redis (~> 3.0) resque (~> 1.25) rufus-scheduler (~> 3.0) - rufus-scheduler (3.1.3) - sass (3.4.17) + rufus-scheduler (3.1.4) + sass (3.4.18) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sequel (4.25.0) + sequel (4.26.0) serverengine (1.5.10) sigdump (~> 0.2.2) sidekiq (3.4.2) @@ -265,8 +267,8 @@ GEM sigdump (0.2.3) sinatra (1.0) rack (>= 1.0) - sneakers (1.1.1) - bunny (>= 1.7.0, <= 2.0.0) + sneakers (2.2.0) + bunny (~> 2.2.0) serverengine (~> 1.5.5) thor thread (~> 0.1.7) @@ -277,13 +279,13 @@ GEM thor (0.19.1) thread (0.1.7) thread_safe (0.3.5) - timers (4.0.1) + timers (4.0.4) hitimes - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.7.1) + tzinfo-data (1.2015.6) + tzinfo (>= 1.0.0) + uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) vegas (0.1.11) @@ -318,8 +320,8 @@ DEPENDENCIES minitest (< 5.3.4) mocha (~> 0.14) mysql (>= 2.9.0) - mysql2 (>= 0.4.0)! - nokogiri (>= 1.4.5) + mysql2 (>= 0.4.0) + nokogiri (>= 1.6.7.rc3) pg (>= 0.18.0) psych (~> 2.0) qu-rails! @@ -345,7 +347,8 @@ DEPENDENCIES sqlite3 (~> 1.3.6) stackprof sucker_punch - turbolinks + turbolinks! + tzinfo-data uglifier (>= 1.3.0) w3c_validators diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 291a8c1e34..312dd1997c 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -49,3 +49,10 @@ module ActionMailer autoload :MessageDelivery autoload :DeliveryJob end + +autoload :Mime, 'action_dispatch/http/mime_type' + +ActiveSupport.on_load(:action_view) do + ActionView::Base.default_formats ||= Mime::SET.symbols + ActionView::Template::Types.delegate_to Mime +end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 6b2e6a531c..50f2c71737 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -449,6 +449,13 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Format with any!", email.parts[1].body.encoded) end + test 'explicit without specifying format with format.any' do + error = assert_raises(ArgumentError) do + BaseMailer.explicit_without_specifying_format_with_any.parts + end + assert_equal "You have to supply at least one format", error.message + end + test "explicit multipart with format(Hash)" do email = BaseMailer.explicit_multipart_with_options(true) email.ready_to_send! diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index ceaa4ca83e..d17e774092 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -165,16 +165,18 @@ class MailDeliveryTest < ActiveSupport::TestCase test "non registered delivery methods raises errors" do DeliveryMailer.delivery_method = :unknown - assert_raise RuntimeError do + error = assert_raise RuntimeError do DeliveryMailer.welcome.deliver_now end + assert_equal "Invalid delivery method :unknown", error.message end test "undefined delivery methods raises errors" do DeliveryMailer.delivery_method = nil - assert_raise RuntimeError do + error = assert_raise RuntimeError do DeliveryMailer.welcome.deliver_now end + assert_equal "Delivery method cannot be nil", error.message end test "does not perform deliveries if requested" do diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index bd991e209e..8c2225ce60 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -80,6 +80,12 @@ class BaseMailer < ActionMailer::Base end end + def explicit_without_specifying_format_with_any(hash = {}) + mail(hash) do |format| + format.any + end + end + def explicit_multipart_with_options(include_html = false) mail do |format| format.text(content_transfer_encoding: "base64"){ render "welcome" } diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index f2229d61fb..bb15edee63 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,25 @@ +* ActionDispatch::Response#new no longer applies default headers. If you want + default headers applied to the response object, then call + `ActionDispatch::Response.create`. This change only impacts people who are + directly constructing an `ActionDispatch::Response` object. + +* Accessing mime types via constants like `Mime::HTML` is deprecated. Please + change code like this: + + Mime::HTML + + To this: + + Mime::Type[:HTML] + + This change is so that Rails will not manage a list of constants, and fixes + an issue where if a type isn't registered you could possibly get the wrong + object. + +* `url_for` does not modify its arguments when generating polymorphic URLs. + + *Bernerd Schaefer* + * Make it easier to opt in to `config.force_ssl` and `config.ssl_options` by making them less dangerous to try and easier to disable. diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index ddd56b354a..3b5128cda5 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -7,7 +7,7 @@ module AbstractController const = sym.upcase class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(*args, &block) # def html(*args, &block) - custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block) + custom(Mime::Type[:#{const}], *args, &block) # custom(Mime::Type[:HTML], *args, &block) end # end RUBY end @@ -25,7 +25,7 @@ module AbstractController def method_missing(symbol, &block) const_name = symbol.upcase - unless Mime.const_defined?(const_name) + unless Mime::Type.registered?(const_name) raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ @@ -33,7 +33,7 @@ module AbstractController "format.html { |html| html.tablet { ... } }" end - mime_constant = Mime.const_get(const_name) + mime_constant = Mime::Type[const_name] if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 6db0941b52..78b43f2fbe 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -24,9 +24,9 @@ module AbstractController options = _normalize_render(*args, &block) self.response_body = render_to_body(options) if options[:html] - _set_content_type Mime::HTML.to_s + _set_html_content_type else - _set_content_type _get_content_type(rendered_format) + _set_rendered_content_type rendered_format end self.response_body end @@ -55,7 +55,7 @@ module AbstractController # Returns Content-Type of rendered content # :api: public def rendered_format - Mime::TEXT + Mime::Type[:TEXT] end DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i( @@ -106,11 +106,10 @@ module AbstractController def _process_format(format) end - def _get_content_type(rendered_format) # :nodoc: - rendered_format.to_s + def _set_html_content_type # :nodoc: end - def _set_content_type(type) # :nodoc: + def _set_rendered_content_type(format) # :nodoc: end # Normalize args and options. diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 0727bb8369..04e5922ce8 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -248,6 +248,7 @@ module ActionController MODULES.each do |mod| include mod end + setup_renderer! # Define some internal variables that should not be propagated to the view. PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 030a1f3478..3d72755f1d 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -135,18 +135,11 @@ module ActionController end def self.make_response!(request) - ActionDispatch::Response.new.tap do |res| + ActionDispatch::Response.create.tap do |res| res.request = request end end - def self.build_with_env(env = {}) #:nodoc: - new.tap { |c| - c.set_request! ActionDispatch::Request.new(env) - c.set_response! make_response!(c.request) - } - end - # Delegates to the class' <tt>controller_name</tt> def controller_name self.class.controller_name diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 15d4562abb..fe470552b0 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -34,7 +34,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime::Type[:XML], Mime::Type[:ATOM] # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } # @current_user = user # else @@ -361,7 +361,7 @@ module ActionController # # def authenticate # case request.format - # when Mime::XML, Mime::ATOM + # when Mime::Type[:XML], Mime::Type[:ATOM] # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) } # @current_user = user # else @@ -436,15 +436,17 @@ module ActionController end end - # Parses the token and options out of the token authorization header. If - # the header looks like this: + # Parses the token and options out of the token authorization header. + # The value for the Authorization header is expected to have the prefix + # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # Authorization: Token token="abc", nonce="def" - # Then the returned token is "abc", and the options is {nonce: "def"} + # Then the returned token is <tt>"abc"</tt>, and the options are + # <tt>{nonce: "def"}</tt> # # request - ActionDispatch::Request instance with the current headers. # - # Returns an Array of [String, Hash] if a token is present. - # Returns nil if no token is found. + # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present. + # Returns +nil+ if no token is found. def token_and_options(request) authorization_request = request.authorization.to_s if authorization_request[TOKEN_REGEX] diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 69583f8ab4..c874165816 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -145,8 +145,8 @@ module ActionController def write(string) unless @response.committed? - @response.headers["Cache-Control"] = "no-cache" - @response.headers.delete "Content-Length" + @response.set_header "Cache-Control", "no-cache" + @response.delete_header "Content-Length" end super @@ -236,6 +236,10 @@ module ActionController end end + def initialize(status = 200, header = {}, body = []) + super(status, Header.new(self, header), body) + end + private def before_committed @@ -257,10 +261,6 @@ module ActionController buf end - def merge_default_headers(original, default) - Header.new self, super - end - def handle_conditional_get! super unless committed? end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index e62da0fa70..fc42fe5c07 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -191,7 +191,7 @@ module ActionController #:nodoc: if format = collector.negotiate_format(request) _process_format(format) - _set_content_type _get_content_type format + _set_rendered_content_type format response = collector.response response ? response.call : render({}) else @@ -229,14 +229,14 @@ module ActionController #:nodoc: @responses = {} @variant = variant - mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } + mimes.each { |mime| @responses[Mime::Type[mime.upcase.to_sym]] = nil } end def any(*args, &block) if args.any? args.each { |type| send(type, &block) } else - custom(Mime::ALL, &block) + custom(Mime::Type[:ALL], &block) end end alias :all :any @@ -251,7 +251,7 @@ module ActionController #:nodoc: end def response - response = @responses.fetch(format, @responses[Mime::ALL]) + response = @responses.fetch(format, @responses[Mime::Type[:ALL]]) if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax response.variant elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index e680432127..c38fc40b81 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -276,7 +276,9 @@ module ActionController # Checks if we should perform parameters wrapping. def _wrapper_enabled? - ref = request.content_mime_type.try(:ref) + return false unless request.has_content_type? + + ref = request.content_mime_type.ref _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index cb74c4f0d4..d867c97b46 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -68,11 +68,11 @@ module ActionController # ActionController::Renderers.add :csv do |obj, options| # filename = options[:filename] || 'data' # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s - # send_data str, type: Mime::CSV, + # send_data str, type: Mime::Type[:CSV], # disposition: "attachment; filename=#{filename}.csv" # end # - # Note that we used Mime::CSV for the csv mime type as it comes with Rails. + # Note that we used Mime::Type[:CSV] for the csv mime type as it comes with Rails. # For a custom renderer, you'll need to register a mime type with # <tt>Mime::Type.register</tt>. # @@ -116,24 +116,24 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - if content_type.nil? || content_type == Mime::JSON - self.content_type = Mime::JS + if content_type.nil? || content_type == Mime::Type[:JSON] + self.content_type = Mime::Type[:JS] end "/**/#{options[:callback]}(#{json})" else - self.content_type ||= Mime::JSON + self.content_type ||= Mime::Type[:JSON] json end end add :js do |js, options| - self.content_type ||= Mime::JS + self.content_type ||= Mime::Type[:JS] js.respond_to?(:to_js) ? js.to_js(options) : js end add :xml do |xml, options| - self.content_type ||= Mime::XML + self.content_type ||= Mime::Type[:XML] xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml end end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index c8934b367f..1ecccf9864 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -11,10 +11,17 @@ module ActionController # Documentation at ActionController::Renderer#render delegate :render, to: :renderer - # Returns a renderer class (inherited from ActionController::Renderer) + # Returns a renderer instance (inherited from ActionController::Renderer) # for the controller. - def renderer - @renderer ||= Renderer.for(self) + attr_reader :renderer + + def setup_renderer! # :nodoc: + @renderer = Renderer.for(self) + end + + def inherited(klass) + klass.setup_renderer! + super end end @@ -56,12 +63,14 @@ module ActionController nil end - def _get_content_type(rendered_format) - self.content_type || super + def _set_html_content_type + self.content_type = Mime::Type[:HTML].to_s end - def _set_content_type(format) - self.content_type = format + def _set_rendered_content_type(format) + unless response.content_type + self.content_type = format.to_s + end end # Normalize arguments by catching blocks and setting them on :update. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index e5f3cb8e8d..5674eef67b 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -137,8 +137,8 @@ module ActionController #:nodoc: def handle_unverified_request request = @controller.request request.session = NullSessionHash.new(request) - request.env['action_dispatch.request.flash_hash'] = nil - request.env['rack.session.options'] = { skip: true } + request.flash = nil + request.session_options = { skip: true } request.cookie_jar = NullCookieJar.build(request, {}) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index bf5c7003ff..903dba3eb4 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -97,9 +97,8 @@ module ActionController # environment they should only be set once at boot-time and never mutated at # runtime. # - # <tt>ActionController::Parameters</tt> inherits from - # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means - # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. + # You can fetch values of <tt>ActionController::Parameters</tt> using either + # <tt>:key</tt> or <tt>"key"</tt>. # # params = ActionController::Parameters.new(key: 'value') # params[:key] # => "value" diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 47d940f692..b2b3b4283f 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -2,12 +2,6 @@ module ActionController module Testing extend ActiveSupport::Concern - # TODO : Rewrite tests using controller.headers= to use Rack env - def headers=(new_headers) - @_response ||= ActionDispatch::Response.new - @_response.headers.replace(new_headers) - end - # Behavior specific to functional tests module Functional # :nodoc: def set_response!(request) diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index e8b29c5b5e..e4d19e9dba 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -34,67 +34,78 @@ module ActionController # ApplicationController.renderer.new(method: 'post', https: true) # class Renderer - class_attribute :controller, :defaults - # Rack environment to render templates in. - attr_reader :env + attr_reader :defaults, :controller - class << self - delegate :render, to: :new + DEFAULTS = { + http_host: 'example.org', + https: false, + method: 'get', + script_name: '', + input: '' + }.freeze - # Create a new renderer class for a specific controller class. - def for(controller) - Class.new self do - self.controller = controller - self.defaults = { - http_host: 'example.org', - https: false, - method: 'get', - script_name: '', - 'rack.input' => '' - } - end - end + # Create a new renderer instance for a specific controller class. + def self.for(controller, env = {}, defaults = DEFAULTS) + new(controller, env, defaults) + end + + # Create a new renderer for the same controller but with a new env. + def new(env = {}) + self.class.new controller, env, defaults + end + + # Create a new renderer for the same controller but with new defaults. + def with_defaults(defaults) + self.class.new controller, env, self.defaults.merge(defaults) end # Accepts a custom Rack environment to render templates in. # It will be merged with ActionController::Renderer.defaults - def initialize(env = {}) - @env = normalize_keys(defaults).merge normalize_keys(env) - @env['action_dispatch.routes'] = controller._routes + def initialize(controller, env, defaults) + @controller = controller + @defaults = defaults + @env = normalize_keys defaults.merge(env) end # Render templates with any options from ActionController::Base#render_to_string. def render(*args) - raise 'missing controller' unless controller? + raise 'missing controller' unless controller - instance = controller.build_with_env(env) + request = ActionDispatch::Request.new @env + request.routes = controller._routes + + instance = controller.new + instance.set_request! request + instance.set_response! controller.make_response!(request) instance.render_to_string(*args) end private def normalize_keys(env) - http_header_format(env).tap do |new_env| - handle_method_key! new_env - handle_https_key! new_env - end + new_env = {} + env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) } + new_env end - def http_header_format(env) - env.transform_keys do |key| - key.is_a?(Symbol) ? key.to_s.upcase : key - end - end + RACK_KEY_TRANSLATION = { + http_host: 'HTTP_HOST', + https: 'HTTPS', + method: 'REQUEST_METHOD', + script_name: 'SCRIPT_NAME', + input: 'rack.input' + } - def handle_method_key!(env) - if method = env.delete('METHOD') - env['REQUEST_METHOD'] = method.upcase - end - end + IDENTITY = ->(_) { _ } + + RACK_VALUE_TRANSLATION = { + https: ->(v) { v ? 'on' : 'off' }, + method: ->(v) { v.upcase }, + } + + def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end - def handle_https_key!(env) - if env.has_key? 'HTTPS' - env['HTTPS'] = env['HTTPS'] ? 'on' : 'off' - end + def rack_value_for(key, value) + RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value end end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 472bb74add..cf78688126 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -33,6 +33,9 @@ module ActionController self.session = session self.session_options = TestSession::DEFAULT_OPTIONS + @custom_param_parsers = { + Mime::Type[:XML] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] } + } end def query_string=(string) @@ -74,23 +77,18 @@ module ActionController set_header k, 'application/x-www-form-urlencoded' end - # FIXME: setting `request_parametes` is normally handled by the - # params parser middleware, and we should remove this roundtripping - # when we switch to caling `call` on the controller - - case content_mime_type.ref + case content_mime_type.to_sym + when nil + raise "Unknown Content-Type: #{content_type}" when :json data = ActiveSupport::JSON.encode(non_path_parameters) - params = ActiveSupport::JSON.decode(data).with_indifferent_access - self.request_parameters = params when :xml data = non_path_parameters.to_xml - params = Hash.from_xml(data)['hash'] - self.request_parameters = params when :url_encoded_form data = non_path_parameters.to_query else - raise "Unknown Content-Type: #{content_type}" + @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } + data = non_path_parameters.to_query end end @@ -133,6 +131,12 @@ module ActionController "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}" end end.new + + private + + def params_parsers + super.merge @custom_param_parsers + end end class LiveTestResponse < Live::Response @@ -398,7 +402,7 @@ module ActionController MSG @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.env['HTTP_ACCEPT'] ||= [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ') __send__(*args).tap do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' @@ -501,7 +505,7 @@ module ActionController if xhr @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest' @request.fetch_header('HTTP_ACCEPT') do |k| - @request.set_header k, [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.set_header k, [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ') end end @@ -581,7 +585,7 @@ module ActionController end def build_response(klass) - klass.new + klass.create end included do diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index cab60a508a..a966c5e452 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,7 +10,7 @@ module ActionDispatch self.ignore_accept_header = false end - # The MIME type of the HTTP request, such as Mime::XML. + # The MIME type of the HTTP request, such as Mime::Type[:XML]. # # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. @@ -29,6 +29,10 @@ module ActionDispatch content_mime_type && content_mime_type.to_s end + def has_content_type? + has_header? 'CONTENT_TYPE' + end + # Returns the accepted MIME type for the request. def accepts fetch_header("action_dispatch.request.accepts") do |k| @@ -45,9 +49,9 @@ module ActionDispatch # Returns the MIME type for the \format used in the request. # - # GET /posts/5.xml | request.format => Mime::XML - # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first + # GET /posts/5.xml | request.format => Mime::Type[:XML] + # GET /posts/5.xhtml | request.format => Mime::Type[:HTML] + # GET /posts/5 | request.format => Mime::Type[:HTML] or Mime::Type[:JS], or request.accepts.first # def format(view_path = []) formats.first || Mime::NullType.instance @@ -66,9 +70,9 @@ module ActionDispatch elsif use_accept_header && valid_accept_header accepts elsif xhr? - [Mime::JS] + [Mime::Type[:JS]] else - [Mime::HTML] + [Mime::Type[:HTML]] end set_header k, v end @@ -134,14 +138,14 @@ module ActionDispatch # def negotiate_mime(order) formats.each do |priority| - if priority == Mime::ALL + if priority == Mime::Type[:ALL] return order.first elsif order.include?(priority) return priority end end - order.include?(Mime::ALL) ? format : nil + order.include?(Mime::Type[:ALL]) ? format : nil end protected diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index a639f8a8f8..a4dfe72c63 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,23 +1,32 @@ -require 'set' require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' +require 'active_support/deprecation' module Mime - class Mimes < Array - def symbols - @symbols ||= map(&:to_sym) + class Mimes + include Enumerable + + def initialize + @mimes = [] + @symbols = nil end - %w(<< concat shift unshift push pop []= clear compact! collect! - delete delete_at delete_if flatten! map! insert reject! reverse! - replace slice! sort! uniq!).each do |method| - module_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{method}(*) - @symbols = nil - super - end - CODE + def each + @mimes.each { |x| yield x } + end + + def <<(type) + @mimes << type + @symbols = nil + end + + def delete_if + @mimes.delete_if { |x| yield x }.tap { @symbols = nil } + end + + def symbols + @symbols ||= map(&:to_sym) end end @@ -35,6 +44,40 @@ module Mime return type if type.is_a?(Type) EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } end + + def const_missing(sym) + if Mime::Type.registered?(sym) + ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated. Please change: + + `Mime::#{sym}` + +to: + + `Mime::Type[:#{sym}]` + eow + Mime::Type[sym] + else + super + end + end + + def const_defined?(sym, inherit = true) + if Mime::Type.registered?(sym) + ActiveSupport::Deprecation.warn <<-eow +Accessing mime types via constants is deprecated. Please change: + + `Mime.const_defined?(#{sym})` + +to: + + `Mime::Type.registered?(:#{sym})` + eow + true + else + super + end + end end # Encapsulates the notion of a mime type. Can be used at render time, for example, with: @@ -51,9 +94,6 @@ module Mime # end # end class Type - @@html_types = Set.new [:html, :all] - cattr_reader :html_types - attr_reader :symbol @register_callbacks = [] @@ -66,7 +106,7 @@ module Mime def initialize(index, name, q = nil) @index = index @name = name - q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list + q ||= 0.0 if @name == Mime::Type[:ALL].to_s # default wildcard match to end of list @q = ((q || 1.0).to_f * 100).to_i end @@ -120,7 +160,7 @@ module Mime end def app_xml_idx - @app_xml_idx ||= index(Mime::XML.to_s) + @app_xml_idx ||= index(Mime::Type[:XML].to_s) end def text_xml @@ -137,6 +177,8 @@ module Mime end end + TYPES = {} + class << self TRAILING_STAR_REGEXP = /(text|application)\/\*/ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ @@ -145,6 +187,18 @@ module Mime @register_callbacks << block end + def registered?(symbol) + TYPES.key? symbol + end + + def [](symbol) + TYPES[symbol] + end + + def add_type(symbol, type) + TYPES[symbol] = type + end + def lookup(string) LOOKUP[string] end @@ -160,17 +214,18 @@ module Mime end def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) - Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms)) + new_mime = Type.new(string, symbol, mime_type_synonyms) + add_type symbol.upcase, new_mime - new_mime = Mime.const_get(symbol.upcase) SET << new_mime - ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup - ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last } + ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup + ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime } @register_callbacks.each do |callback| callback.call(new_mime) end + new_mime end def parse(accept_header) @@ -216,8 +271,7 @@ module Mime # Mime::Type.unregister(:mobile) def unregister(symbol) symbol = symbol.upcase - mime = Mime.const_get(symbol) - Mime.instance_eval { remove_const(symbol) } + mime = TYPES.delete symbol SET.delete_if { |v| v.eql?(mime) } LOOKUP.delete_if { |_,v| v.eql?(mime) } @@ -243,7 +297,7 @@ module Mime end def ref - to_sym || to_s + symbol || to_s end def ===(list) @@ -255,24 +309,23 @@ module Mime end def ==(mime_type) - return false if mime_type.blank? + return false unless mime_type (@synonyms + [ self ]).any? do |synonym| synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym end end def =~(mime_type) - return false if mime_type.blank? + return false unless mime_type regexp = Regexp.new(Regexp.quote(mime_type.to_s)) - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s =~ regexp - end + @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp end def html? - @@html_types.include?(to_sym) || @string =~ /html/ + symbol == :html || @string =~ /html/ end + def all?; false; end private @@ -290,6 +343,11 @@ module Mime def respond_to_missing?(method, include_private = false) #:nodoc: method.to_s.ends_with? '?' end + + class All < Type + def all?; true; end + def html?; true; end + end end class NullType diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 01a10c693b..0af3e5c0df 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -33,4 +33,4 @@ Mime::Type.register "application/pdf", :pdf, [], %w(pdf) Mime::Type.register "application/zip", :zip, [], %w(zip) # Create Mime::ALL but do not add it to the SET. -Mime::ALL = Mime::Type.new("*/*", :all, []) +Mime::Type.add_type :ALL, Mime::Type::All.new("*/*", :all, []) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 3c9f8cd9e4..e3c4392760 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -3,6 +3,20 @@ module ActionDispatch module Parameters PARAMETERS_KEY = 'action_dispatch.request.path_parameters' + DEFAULT_PARSERS = { + Mime::Type[:JSON] => lambda { |raw_post| + data = ActiveSupport::JSON.decode(raw_post) + data.is_a?(Hash) ? data : {:_json => data} + } + } + + def self.included(klass) + class << klass + attr_accessor :parameter_parsers + end + + klass.parameter_parsers = DEFAULT_PARSERS + end # Returns both GET and POST \parameters in a single hash. def parameters params = get_header("action_dispatch.request.parameters") @@ -31,6 +45,27 @@ module ActionDispatch def path_parameters get_header(PARAMETERS_KEY) || {} end + + private + + def parse_formatted_parameters(parsers) + return yield if content_length.zero? + + strategy = parsers.fetch(content_mime_type) { return yield } + + begin + strategy.call(raw_post) + rescue => e # JSON or Ruby code block errors + my_logger = logger || ActiveSupport::Logger.new($stderr) + my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" + + raise ParamsParser::ParseError.new(e.message, e) + end + end + + def params_parsers + ActionDispatch::Request.parameter_parsers + end end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 18504eba6d..eaa7e88b34 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -324,7 +324,7 @@ module ActionDispatch else self.session = {} end - set_header('action_dispatch.request.flash_hash', nil) + self.flash = nil end def session=(session) #:nodoc: @@ -348,8 +348,14 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST fetch_header("action_dispatch.request.request_parameters") do - self.request_parameters = Request::Utils.normalize_encode_params(super || {}) + pr = parse_formatted_parameters(params_parsers) do |params| + super || {} + end + self.request_parameters = Request::Utils.normalize_encode_params(pr) end + rescue ParamsParser::ParseError # one of the parse strategies blew up + self.request_parameters = Request::Utils.normalize_encode_params(super || {}) + raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 4aee489912..cbeea9e267 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -38,8 +38,6 @@ module ActionDispatch # :nodoc: # The HTTP status code. attr_reader :status - attr_writer :sending_file - # Get headers for this response. attr_reader :header @@ -48,20 +46,6 @@ module ActionDispatch # :nodoc: delegate :[], :[]=, :to => :@header delegate :each, :to => :@stream - # Sets the HTTP response's content MIME type. For example, in the controller - # you could write this: - # - # response.content_type = "text/plain" - # - # If a character set has been defined for this response (see charset=) then - # the character set information will also be included in the content type - # information. - attr_reader :content_type - - # The charset of the response. HTML wants to know the encoding of the - # content you're giving them, so we need to send that along. - attr_reader :charset - CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze @@ -119,31 +103,30 @@ module ActionDispatch # :nodoc: end end + def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers) + header = merge_default_headers(header, default_headers) + new status, header, body + end + + def self.merge_default_headers(original, default) + default.respond_to?(:merge) ? default.merge(original) : original + end + # The underlying body, as a streamable object. attr_reader :stream - def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers) + def initialize(status = 200, header = {}, body = []) super() - header = merge_default_headers(header, default_headers) @header = header self.body, self.status = body, status - @sending_file = false @blank = false @cv = new_cond @committed = false @sending = false @sent = false - @content_type = nil - @charset = self.class.default_charset - - if content_type = self[CONTENT_TYPE] - type, charset = content_type.split(/;\s*charset=/) - @content_type = Mime::Type.lookup(type) - @charset = charset || self.class.default_charset - end prepare_cache_control! @@ -199,7 +182,27 @@ module ActionDispatch # :nodoc: # Sets the HTTP content type. def content_type=(content_type) - @content_type = content_type.to_s + header_info = parse_content_type + set_content_type content_type.to_s, header_info.charset || self.class.default_charset + end + + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # If a character set has been defined for this response (see charset=) then + # the character set information will also be included in the content type + # information. + + def content_type + parse_content_type.mime_type + end + + def sending_file=(v) + if true == v + self.charset = false + end end # Sets the HTTP character set. In case of nil parameter @@ -208,7 +211,20 @@ module ActionDispatch # :nodoc: # response.charset = 'utf-16' # => 'utf-16' # response.charset = nil # => 'utf-8' def charset=(charset) - @charset = charset.nil? ? self.class.default_charset : charset + header_info = parse_content_type + if false == charset + set_header CONTENT_TYPE, header_info.mime_type + else + content_type = header_info.mime_type + set_content_type content_type, charset || self.class.default_charset + end + end + + # The charset of the response. HTML wants to know the encoding of the + # content you're giving them, so we need to send that along. + def charset + header_info = parse_content_type + header_info.charset || self.class.default_charset end # The response code of the request. @@ -308,6 +324,26 @@ module ActionDispatch # :nodoc: private + ContentTypeHeader = Struct.new :mime_type, :charset + NullContentTypeHeader = ContentTypeHeader.new nil, nil + + def parse_content_type + content_type = get_header CONTENT_TYPE + if content_type + type, charset = content_type.split(/;\s*charset=/) + type = nil if type.empty? + ContentTypeHeader.new(type, charset) + else + NullContentTypeHeader + end + end + + def set_content_type(content_type, charset) + type = (content_type || '').dup + type << "; charset=#{charset}" if charset + set_header CONTENT_TYPE, type + end + def before_committed return if committed? assign_default_content_type_and_charset! @@ -317,10 +353,6 @@ module ActionDispatch # :nodoc: def before_sending end - def merge_default_headers(original, default) - default.respond_to?(:merge) ? default.merge(original) : original - end - def build_buffer(response, body) Buffer.new response, body end @@ -330,18 +362,11 @@ module ActionDispatch # :nodoc: end def assign_default_content_type_and_charset! - return if get_header(CONTENT_TYPE).present? - - @content_type ||= Mime::HTML - - type = @content_type.to_s.dup - type << "; charset=#{charset}" if append_charset? - - set_header CONTENT_TYPE, type - end + return if content_type - def append_charset? - !@sending_file && @charset != false + ct = parse_content_type + set_content_type(ct.mime_type || Mime::Type[:HTML].to_s, + ct.charset || self.class.default_charset) end class RackBody @@ -381,7 +406,7 @@ module ActionDispatch # :nodoc: end def rack_response(status, header) - if NO_CONTENT_CODES.include?(@status) + if NO_CONTENT_CODES.include?(status) header.delete CONTENT_TYPE header.delete 'Content-Length' [status, header, []] diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 47bf76bdbf..7063b44bb5 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - module ActionDispatch module Journey # :nodoc: module NFA # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 537c9b2f5c..306d2e674a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - module ActionDispatch module Journey # :nodoc: class Format diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index f37439e4d7..b653e4eacd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -221,19 +221,11 @@ module ActionDispatch 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 @@ -253,6 +245,11 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature nil end + + private + def parse(name, signed_message) + super || verify_and_upgrade_legacy_signed_message(name, signed_message) + end end class CookieJar #:nodoc: @@ -412,7 +409,7 @@ module ActionDispatch end end - class PermanentCookieJar #:nodoc: + class AbstractCookieJar # :nodoc: include ChainedCookieJars def initialize(parent_jar) @@ -420,19 +417,35 @@ module ActionDispatch end def [](name) - @parent_jar[name.to_s] + if data = @parent_jar[name.to_s] + parse name, data + end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else - options = { :value => options } + options = { value: options } end - options[:expires] = 20.years.from_now + commit(options) @parent_jar[name] = options end + + protected + def request; @parent_jar.request; end + + private + def parse(name, data); data; end + def commit(options); end + end + + class PermanentCookieJar < AbstractCookieJar # :nodoc: + private + def commit(options) + options[:expires] = 20.years.from_now + end end class JsonSerializer # :nodoc: @@ -484,45 +497,30 @@ module ActionDispatch def digest request.cookies_digest || 'SHA1' end + + def key_generator + request.key_generator + end end - class SignedCookieJar #:nodoc: - include ChainedCookieJars + class SignedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars def initialize(parent_jar) - @parent_jar = parent_jar + super secret = key_generator.generate_key(request.signed_cookie_salt) @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - # Returns the value of the cookie by +name+ if it is untampered, - # returns +nil+ otherwise or if no such cookie exists. - def [](name) - if signed_message = @parent_jar[name] - deserialize name, verify(signed_message) + private + def parse(name, signed_message) + deserialize name, @verifier.verified(signed_message) end - end - # Signs and sets the cookie named +name+. The second argument may be the cookie's - # value or a hash of options as documented above. - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! + def commit(options) options[:value] = @verifier.generate(serialize(options[:value])) - else - options = { :value => @verifier.generate(serialize(options)) } - end - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - - private - def verify(signed_message) - @verifier.verify(signed_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end @@ -532,20 +530,13 @@ module ActionDispatch # re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if signed_message = @parent_jar[name] - deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) - end - end end - class EncryptedCookieJar #:nodoc: - include ChainedCookieJars + class EncryptedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars def initialize(parent_jar) - @parent_jar = parent_jar + super if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + @@ -557,35 +548,18 @@ module ActionDispatch @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end - # Returns the value of the cookie by +name+ if it is untampered, - # returns +nil+ otherwise or if no such cookie exists. - def [](name) - if encrypted_message = @parent_jar[name] - deserialize name, decrypt_and_verify(encrypted_message) - end - end - - # Encrypts and sets the cookie named +name+. The second argument may be the cookie's - # value or a hash of options as documented above. - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! - else - options = { :value => options } - end - - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) - - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options - end - private - def decrypt_and_verify(encrypted_message) - @encryptor.decrypt_and_verify(encrypted_message) + def parse(name, encrypted_message) + deserialize name, @encryptor.decrypt_and_verify(encrypted_message) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end + + def commit(options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE + end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore @@ -594,12 +568,6 @@ module ActionDispatch # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage - - def [](name) - if encrypted_or_signed_message = @parent_jar[name] - deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) - end - end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 039efc3af8..5fd984cd07 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -61,7 +61,7 @@ module ActionDispatch end def traces - appplication_trace_with_ids = [] + application_trace_with_ids = [] framework_trace_with_ids = [] full_trace_with_ids = [] @@ -69,7 +69,7 @@ module ActionDispatch trace_with_id = { id: idx, trace: trace } if application_trace.include?(trace) - appplication_trace_with_ids << trace_with_id + application_trace_with_ids << trace_with_id else framework_trace_with_ids << trace_with_id end @@ -78,7 +78,7 @@ module ActionDispatch end { - "Application Trace" => appplication_trace_with_ids, + "Application Trace" => application_trace_with_ids, "Framework Trace" => framework_trace_with_ids, "Full Trace" => full_trace_with_ids } diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index c482b1c5e7..014b7bee61 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -276,7 +276,7 @@ module ActionDispatch req = ActionDispatch::Request.new env @app.call(env) ensure - session = Request::Session.find(req) || {} + session = req.session || {} flash_hash = req.flash_hash if flash_hash && (flash_hash.present? || session.key?('flash')) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 9cde9c9b98..18af0a583a 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -18,48 +18,13 @@ module ActionDispatch end end - DEFAULT_PARSERS = { - Mime::JSON => lambda { |raw_post| - data = ActiveSupport::JSON.decode(raw_post) - data = {:_json => data} unless data.is_a?(Hash) - Request::Utils.normalize_encode_params(data) - } - } - # Create a new +ParamsParser+ middleware instance. # # The +parsers+ argument can take Hash of parsers where key is identifying # content mime type, and value is a lambda that is going to process data. - def initialize(app, parsers = {}) - @app, @parsers = app, DEFAULT_PARSERS.merge(parsers) + def self.new(app, parsers = {}) + ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers) + app end - - def call(env) - request = Request.new(env) - - parse_formatted_parameters(request, @parsers) do |params| - request.request_parameters = params - end - - @app.call(env) - end - - private - def parse_formatted_parameters(request, parsers) - return if request.content_length.zero? - - strategy = parsers.fetch(request.content_mime_type) { return nil } - - yield strategy.call(request.raw_post) - - rescue => e # JSON or Ruby code block errors - logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" - - raise ParseError.new(e.message, e) - end - - def logger(request) - request.logger || ActiveSupport::Logger.new($stderr) - end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 3f7011d100..02b6cfe727 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -53,7 +53,7 @@ module ActionDispatch # # Note that changing the secret key will invalidate all existing sessions! # - # Because CookieStore extends Rack::Session::Abstract::ID, many of the + # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the # options described there can be used to customize the session cookie that # is generated. For example: # diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index b72953f1d1..47f475559a 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -15,7 +15,8 @@ module ActionDispatch # # Configure HSTS with `hsts: { … }`: # * `expires`: How long, in seconds, these settings will stick. Defaults to - # `18.weeks`, the minimum required to qualify for browser preload lists. + # `180.days` (recommended). The minimum required to qualify for browser + # preload lists is `18.weeks`. # * `subdomains`: Set to `true` to tell the browser to apply these settings # to all subdomains. This protects your cookies from interception by a # vulnerable site on a subdomain. Defaults to `false`. diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 8757c9ea7f..f3c6be864f 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces @@ -148,6 +146,7 @@ module ActionDispatch # get 'geocode/:postalcode' => :show, constraints: { # postalcode: /\d{5}(-\d{4})?/ # } + # end # # Constraints can include the 'ignorecase' and 'extended syntax' regular # expression modifiers: diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 967bbd62f8..883cd9c2c3 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -180,7 +180,8 @@ module ActionDispatch when Symbol HelperMethodBuilder.url.handle_string_call self, options when Array - polymorphic_url(options, options.extract_options!) + components = options.dup + polymorphic_url(components, components.extract_options!) when Class HelperMethodBuilder.url.handle_class_call self, options else diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 21b3b89d22..81fa10a613 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type === Mime::XML + @html_document ||= if @response.content_type === Mime::Type[:XML] Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 4dfd4f3f71..753cd2073b 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -354,7 +354,7 @@ module ActionDispatch if xhr headers ||= {} headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + headers['HTTP_ACCEPT'] ||= [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ') end # this modifies the passed request_env directly diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 6a31d6243f..4b79a90242 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -7,7 +7,7 @@ module ActionDispatch # See Response for more information on controller response objects. class TestResponse < Response def self.from_response(response) - new response.status, response.headers, response.body, default_headers: nil + new response.status, response.headers, response.body end # Was the response successful? diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index fc59bf19c4..3b36e43c0b 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -53,9 +53,9 @@ module AbstractController collector.html collector.text(:foo) collector.js(:bar) { :baz } - assert_equal [Mime::HTML, [], nil], collector.responses[0] - assert_equal [Mime::TEXT, [:foo], nil], collector.responses[1] - assert_equal [Mime::JS, [:bar]], collector.responses[2][0,2] + assert_equal [Mime::Type[:HTML], [], nil], collector.responses[0] + assert_equal [Mime::Type[:TEXT], [:foo], nil], collector.responses[1] + assert_equal [Mime::Type[:JS], [:bar]], collector.responses[2][0,2] assert_equal :baz, collector.responses[2][2].call end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 3c498960e4..ef7aab72c6 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -104,7 +104,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase 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 diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index beeafc2e53..7dfeadceb0 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -65,7 +65,7 @@ class ActionPackAssertionsController < ActionController::Base end def render_text_with_custom_content_type - render body: "Hello!", content_type: Mime::RSS + render body: "Hello!", content_type: Mime::Type[:RSS] end def session_stuffing diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index c5bbc479c9..4a86f1bad3 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' class OldContentTypeController < ActionController::Base # :ported: def render_content_type_from_body - response.content_type = Mime::RSS + response.content_type = Mime::Type[:RSS] render body: "hello world!" end @@ -14,7 +14,7 @@ class OldContentTypeController < ActionController::Base # :ported: def render_content_type_from_render - render body: "hello world!", :content_type => Mime::RSS + render body: "hello world!", :content_type => Mime::Type[:RSS] end # :ported: @@ -36,7 +36,7 @@ class OldContentTypeController < ActionController::Base end def render_change_for_builder - response.content_type = Mime::HTML + response.content_type = Mime::Type[:HTML] render :action => "render_default_for_builder" end @@ -45,7 +45,7 @@ class OldContentTypeController < ActionController::Base format.html { render body: "hello world!" } format.xml { render action: "render_default_content_types_for_respond_to" } format.js { render body: "hello world!" } - format.rss { render body: "hello world!", content_type: Mime::XML } + format.rss { render body: "hello world!", content_type: Mime::Type[:XML] } end end end @@ -64,68 +64,68 @@ class ContentTypeTest < ActionController::TestCase def test_render_defaults get :render_defaults assert_equal "utf-8", @response.charset - assert_equal Mime::TEXT, @response.content_type + assert_equal Mime::Type[:TEXT], @response.content_type end def test_render_changed_charset_default with_default_charset "utf-16" do get :render_defaults assert_equal "utf-16", @response.charset - assert_equal Mime::TEXT, @response.content_type + assert_equal Mime::Type[:TEXT], @response.content_type end end # :ported: def test_content_type_from_body get :render_content_type_from_body - assert_equal Mime::RSS, @response.content_type + assert_equal Mime::Type[:RSS], @response.content_type assert_equal "utf-8", @response.charset end # :ported: def test_content_type_from_render get :render_content_type_from_render - assert_equal Mime::RSS, @response.content_type + assert_equal Mime::Type[:RSS], @response.content_type assert_equal "utf-8", @response.charset end # :ported: def test_charset_from_body get :render_charset_from_body - assert_equal Mime::TEXT, @response.content_type + assert_equal Mime::Type[:TEXT], @response.content_type assert_equal "utf-16", @response.charset end # :ported: def test_nil_charset_from_body get :render_nil_charset_from_body - assert_equal Mime::TEXT, @response.content_type + assert_equal Mime::Type[:TEXT], @response.content_type assert_equal "utf-8", @response.charset, @response.headers.inspect end def test_nil_default_for_erb with_default_charset nil do get :render_default_for_erb - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::Type[:HTML], @response.content_type assert_nil @response.charset, @response.headers.inspect end end def test_default_for_erb get :render_default_for_erb - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::Type[:HTML], @response.content_type assert_equal "utf-8", @response.charset end def test_default_for_builder get :render_default_for_builder - assert_equal Mime::XML, @response.content_type + assert_equal Mime::Type[:XML], @response.content_type assert_equal "utf-8", @response.charset end def test_change_for_builder get :render_change_for_builder - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::Type[:HTML], @response.content_type assert_equal "utf-8", @response.charset end @@ -144,24 +144,24 @@ class AcceptBasedContentTypeTest < ActionController::TestCase tests OldContentTypeController def test_render_default_content_types_for_respond_to - @request.accept = Mime::HTML.to_s + @request.accept = Mime::Type[:HTML].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::HTML, @response.content_type + assert_equal Mime::Type[:HTML], @response.content_type - @request.accept = Mime::JS.to_s + @request.accept = Mime::Type[:JS].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::JS, @response.content_type + assert_equal Mime::Type[:JS], @response.content_type end def test_render_default_content_types_for_respond_to_with_template - @request.accept = Mime::XML.to_s + @request.accept = Mime::Type[:XML].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::XML, @response.content_type + assert_equal Mime::Type[:XML], @response.content_type end def test_render_default_content_types_for_respond_to_with_overwrite - @request.accept = Mime::RSS.to_s + @request.accept = Mime::Type[:RSS].to_s get :render_default_content_types_for_respond_to - assert_equal Mime::XML, @response.content_type + assert_equal Mime::Type[:XML], @response.content_type end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index e9c19b7acf..4d1c23cbee 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -112,7 +112,7 @@ module ActionController class TestController < ActionController::Base include ActionController::Live - attr_accessor :latch, :tc + attr_accessor :latch, :tc, :error_latch def self.controller_path 'test' @@ -204,6 +204,12 @@ module ActionController end def overfill_buffer_and_die + logger = ActionController::Base.logger || Logger.new($stdout) + response.stream.on_error do + logger.warn 'Error while streaming' + error_latch.count_down + end + # Write until the buffer is full. It doesn't expose that # information directly, so we must hard-code its size: 10.times do @@ -256,20 +262,12 @@ module ActionController end def test_set_cookie - @controller = TestController.new get :set_cookie assert_equal({'hello' => 'world'}, @response.cookies) assert_equal "hello world", @response.body end - def test_set_response! - @controller.set_response!(@request) - assert_kind_of(Live::Response, @controller.response) - assert_equal @request, @controller.response.request - end - def test_write_to_stream - @controller = TestController.new get :basic_stream assert_equal "helloworld", @response.body assert_equal 'text/event-stream', @response.headers['Content-Type'] @@ -281,10 +279,9 @@ module ActionController @controller.latch = Concurrent::CountDownLatch.new parts = ['hello', 'world'] - @controller.request = @request - @controller.response = @response + get :blocking_stream - t = Thread.new(@response) { |resp| + t = Thread.new(response) { |resp| resp.await_commit resp.stream.each do |part| assert_equal parts.shift, part @@ -294,38 +291,28 @@ module ActionController end } - @controller.process :blocking_stream - assert t.join(3), 'timeout expired before the thread terminated' end def test_abort_with_full_buffer @controller.latch = Concurrent::CountDownLatch.new - - @request.parameters[:format] = 'plain' - @controller.request = @request - @controller.response = @response - - got_error = Concurrent::CountDownLatch.new - @response.stream.on_error do - ActionController::Base.logger.warn 'Error while streaming' - got_error.count_down - end - - t = Thread.new(@response) { |resp| - resp.await_commit - _, _, body = resp.to_a - body.each do - @controller.latch.wait - body.close - break - end - } + @controller.error_latch = Concurrent::CountDownLatch.new capture_log_output do |output| - @controller.process :overfill_buffer_and_die + get :overfill_buffer_and_die, :format => 'plain' + + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + @controller.latch.wait + body.close + break + end + } + t.join - got_error.wait + @controller.error_latch.wait assert_match 'Error while streaming', output.rewind && output.read end end @@ -333,20 +320,18 @@ module ActionController def test_ignore_client_disconnect @controller.latch = Concurrent::CountDownLatch.new - @controller.request = @request - @controller.response = @response + capture_log_output do |output| + get :ignore_client_disconnect - t = Thread.new(@response) { |resp| - resp.await_commit - _, _, body = resp.to_a - body.each do - body.close - break - end - } + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + body.close + break + end + } - capture_log_output do |output| - @controller.process :ignore_client_disconnect t.join Timeout.timeout(3) do @controller.latch.wait @@ -364,11 +349,8 @@ module ActionController end def test_live_stream_default_header - @controller.request = @request - @controller.response = @response - @controller.process :default_header - _, headers, _ = @response.prepare! - assert headers['Content-Type'] + get :default_header + assert response.headers['Content-Type'] end def test_render_text @@ -437,13 +419,13 @@ module ActionController def test_stale_without_etag get :with_stale - assert_equal 200, @response.status.to_i + assert_equal 200, response.status.to_i end def test_stale_with_etag @request.if_none_match = Digest::MD5.hexdigest("123") get :with_stale - assert_equal 304, @response.status.to_i + assert_equal 304, response.status.to_i end end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index 0445a837ca..d9899fe01f 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -7,12 +7,12 @@ module ContentType end def set_on_response_obj - response.content_type = Mime::RSS + response.content_type = Mime::Type[:RSS] render body: "Hello world!" end def set_on_render - render body: "Hello world!", content_type: Mime::RSS + render body: "Hello world!", content_type: Mime::Type[:RSS] end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 91b30ede6a..631ff7d02a 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -266,15 +266,17 @@ class RedirectTest < ActionController::TestCase end def test_redirect_to_nil - assert_raise(ActionController::ActionControllerError) do + error = assert_raise(ActionController::ActionControllerError) do get :redirect_to_nil end + assert_equal "Cannot redirect to nil!", error.message end def test_redirect_to_params - assert_raise(ActionController::ActionControllerError) do + error = assert_raise(ActionController::ActionControllerError) do get :redirect_to_params end + assert_equal "Cannot redirect to a parameter hash!", error.message end def test_redirect_to_with_block diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb index af50e11261..8891f6177f 100644 --- a/actionpack/test/controller/render_other_test.rb +++ b/actionpack/test/controller/render_other_test.rb @@ -12,7 +12,7 @@ class RenderOtherTest < ActionController::TestCase def test_using_custom_render_option ActionController.add_renderer :simon do |says, options| - self.content_type = Mime::TEXT + self.content_type = Mime::Type[:TEXT] self.response_body = "Simon says: #{says}" end diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb index 7a91577b17..094d3ea1d2 100644 --- a/actionpack/test/controller/render_xml_test.rb +++ b/actionpack/test/controller/render_xml_test.rb @@ -92,6 +92,6 @@ class RenderXmlTest < ActionController::TestCase def test_should_use_implicit_content_type get :implicit_content_type, format: 'atom' - assert_equal Mime::ATOM, @response.content_type + assert_equal Mime::Type[:ATOM], @response.content_type end end diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb index b55a25430b..16d24fa82a 100644 --- a/actionpack/test/controller/renderer_test.rb +++ b/actionpack/test/controller/renderer_test.rb @@ -1,6 +1,10 @@ require 'abstract_unit' class RendererTest < ActiveSupport::TestCase + test 'action controller base has a renderer' do + assert ActionController::Base.renderer + end + test 'creating with a controller' do controller = CommentsController renderer = ActionController::Renderer.for controller @@ -57,8 +61,7 @@ class RendererTest < ActiveSupport::TestCase end test 'rendering with defaults' do - renderer = ApplicationController.renderer - renderer.defaults[:https] = true + renderer = ApplicationController.renderer.new https: true content = renderer.render inline: '<%= request.ssl? %>' assert_equal 'true', content @@ -67,8 +70,8 @@ class RendererTest < ActiveSupport::TestCase test 'same defaults from the same controller' do renderer_defaults = ->(controller) { controller.renderer.defaults } - assert renderer_defaults[AccountsController].equal? renderer_defaults[AccountsController] - assert_not renderer_defaults[AccountsController].equal? renderer_defaults[CommentsController] + assert_equal renderer_defaults[AccountsController], renderer_defaults[AccountsController] + assert_equal renderer_defaults[AccountsController], renderer_defaults[CommentsController] end test 'rendering with different formats' do @@ -83,18 +86,6 @@ class RendererTest < ActiveSupport::TestCase test 'rendering with helpers' do assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>'] end - - test 'rendering from inherited renderer' do - inherited = Class.new ApplicationController.renderer do - defaults[:script_name] = 'script' - def render(options) - super options.merge(locals: { param: :value }) - end - end - - template = '<%= url_for controller: :foo, action: :bar, param: param %>' - assert_equal 'script/foo/bar?param=value', inherited.render(inline: template) - end private def render diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index c0ddcf7f50..c712c75c88 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -20,6 +20,47 @@ class SendFileController < ActionController::Base send_file(file_path, options) end + def test_send_file_headers_bang + options = { + :type => Mime::Type[:PNG], + :disposition => 'disposition', + :filename => 'filename' + } + + send_data "foo", options + end + + def test_send_file_headers_with_disposition_as_a_symbol + options = { + :type => Mime::Type[:PNG], + :disposition => :disposition, + :filename => 'filename' + } + + send_data "foo", options + end + + def test_send_file_headers_with_mime_lookup_with_symbol + options = { :type => :png } + + send_data "foo", options + end + + def test_send_file_headers_with_bad_symbol + options = { :type => :this_type_is_not_registered } + send_data "foo", options + end + + def test_send_file_headers_with_nil_content_type + options = { :type => nil } + send_data "foo", options + end + + def test_send_file_headers_guess_type_from_extension + options = { :filename => params[:filename] } + send_data "foo", options + end + def data send_data(file_data, options) end @@ -88,62 +129,39 @@ class SendFileTest < ActionController::TestCase # Test that send_file_headers! is setting the correct HTTP headers. def test_send_file_headers_bang - options = { - :type => Mime::PNG, - :disposition => 'disposition', - :filename => 'filename' - } - # Do it a few times: the resulting headers should be identical # no matter how many times you send with the same options. # Test resolving Ticket #458. - @controller.headers = {} - @controller.send(:send_file_headers!, options) - @controller.send(:send_file_headers!, options) - @controller.send(:send_file_headers!, options) - - h = @controller.headers - assert_equal 'image/png', @controller.content_type - assert_equal 'disposition; filename="filename"', h['Content-Disposition'] - assert_equal 'binary', h['Content-Transfer-Encoding'] + 5.times do + get :test_send_file_headers_bang - # test overriding Cache-Control: no-cache header to fix IE open/save dialog - @controller.send(:send_file_headers!, options) - @controller.response.prepare! - assert_equal 'private', h['Cache-Control'] + assert_equal 'image/png', response.content_type + assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition') + assert_equal 'binary', response.get_header('Content-Transfer-Encoding') + assert_equal 'private', response.get_header('Cache-Control') + end end def test_send_file_headers_with_disposition_as_a_symbol - options = { - :type => Mime::PNG, - :disposition => :disposition, - :filename => 'filename' - } + get :test_send_file_headers_with_disposition_as_a_symbol - @controller.headers = {} - @controller.send(:send_file_headers!, options) - assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition'] + assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition') end def test_send_file_headers_with_mime_lookup_with_symbol - options = { - :type => :png - } - - @controller.headers = {} - @controller.send(:send_file_headers!, options) - - assert_equal 'image/png', @controller.content_type + get __method__ + assert_equal 'image/png', response.content_type end def test_send_file_headers_with_bad_symbol - options = { - :type => :this_type_is_not_registered - } + error = assert_raise(ArgumentError) { get __method__ } + assert_equal "Unknown MIME type this_type_is_not_registered", error.message + end - @controller.headers = {} - assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) } + def test_send_file_headers_with_nil_content_type + error = assert_raise(ArgumentError) { get __method__ } + assert_equal ":type option required", error.message end def test_send_file_headers_guess_type_from_extension @@ -158,10 +176,8 @@ class SendFileTest < ActionController::TestCase 'file.unk' => 'application/octet-stream', 'zip' => 'application/octet-stream' }.each do |filename,expected_type| - options = { :filename => filename } - @controller.headers = {} - @controller.send(:send_file_headers!, options) - assert_equal expected_type, @controller.content_type + get __method__, params: { filename: filename } + assert_equal expected_type, response.content_type end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index b3c3979c84..40c97abd35 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -627,6 +627,31 @@ XML assert_equal "application/json", parsed_env["CONTENT_TYPE"] end + def test_mutating_content_type_headers_for_plain_text_files_sets_the_header + @request.headers['Content-Type'] = 'text/plain' + post :render_body, params: { name: 'foo.txt' } + + assert_equal 'text/plain', @request.headers['Content-type'] + assert_equal 'foo.txt', @request.request_parameters[:name] + assert_equal 'render_body', @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_html_files_sets_the_header + @request.headers['Content-Type'] = 'text/html' + post :render_body, params: { name: 'foo.html' } + + assert_equal 'text/html', @request.headers['Content-type'] + assert_equal 'foo.html', @request.request_parameters[:name] + assert_equal 'render_body', @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_non_registered_mime_type_raises_an_error + assert_raises(RuntimeError) do + @request.headers['Content-Type'] = 'type/fake' + post :render_body, params: { name: 'foo.fake' } + end + end + def test_id_converted_to_string get :test_params, params: { id: 20, foo: Object.new @@ -949,6 +974,11 @@ class ResponseDefaultHeadersTest < ActionController::TestCase headers.delete params[:header] head :ok, 'C' => '3' end + + # Render a head response, but don't touch default headers + def leave_alone + head :ok + end end def before_setup @@ -974,9 +1004,13 @@ class ResponseDefaultHeadersTest < ActionController::TestCase end test "response contains default headers" do + get :leave_alone + # Response headers start out with the defaults - assert_equal @defaults, response.headers + assert_equal @defaults.merge('Content-Type' => 'text/html'), response.headers + end + test "response deletes a default header" do get :remove_header, params: { header: 'A' } assert_response :ok diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 31677f202d..78e883f134 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -451,6 +451,26 @@ module AbstractController end end + def test_url_for_with_array_is_unmodified + with_routing do |set| + set.draw do + namespace :admin do + resources :posts + end + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = 'www.basecamphq.com' + + original_components = [:new, :admin, :post, { param: 'value' }] + components = original_components.dup + + kls.new.url_for(components) + + assert_equal(original_components, components) + end + end + private def extract_params(url) url.split('?', 2).last.split('&').sort diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index b26f037c36..2aee914a24 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -65,7 +65,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_register_and_use_json_simple with_test_route_set do - with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do + with_params_parsers Mime::Type[:JSON] => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do post "/", params: '{"request":{"summary":"content...","title":"JSON"}}', headers: { 'CONTENT_TYPE' => 'application/json' } @@ -97,24 +97,28 @@ class WebServiceTest < ActionDispatch::IntegrationTest end def test_parsing_json_doesnot_rescue_exception - with_test_route_set do - with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do - assert_raises(Interrupt) do - post "/", - params: '{"title":"JSON"}}', - headers: { 'CONTENT_TYPE' => 'application/json' } - end + req = Class.new(ActionDispatch::Request) do + def params_parsers + { Mime::Type[:JSON] => Proc.new { |data| raise Interrupt } } end + + def content_length; get_header('rack.input').length; end + end.new({ 'rack.input' => StringIO.new('{"title":"JSON"}}'), 'CONTENT_TYPE' => 'application/json' }) + + assert_raises(Interrupt) do + req.request_parameters end end private def with_params_parsers(parsers = {}) old_session = @integration_session - @app = ActionDispatch::ParamsParser.new(app.routes, parsers) + original_parsers = ActionDispatch::Request.parameter_parsers + ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers reset! yield ensure + ActionDispatch::Request.parameter_parsers = original_parsers @integration_session = old_session end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 3017a9c2d6..d9fb44f06d 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -13,76 +13,75 @@ class MimeTypeTest < ActiveSupport::TestCase test "unregister" do begin Mime::Type.register("text/x-mobile", :mobile) - assert defined?(Mime::MOBILE) - assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile'] - assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile'] + assert Mime::Type.registered?(:MOBILE) + assert_equal Mime::Type[:MOBILE], Mime::LOOKUP['text/x-mobile'] + assert_equal Mime::Type[:MOBILE], Mime::EXTENSION_LOOKUP['mobile'] Mime::Type.unregister(:mobile) - assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined" + assert !Mime.const_defined?(:MOBILE), "Mime::MOBILE should not be defined" assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]" assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]" ensure - Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) } Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'} end end test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] + expect = [Mime::Type[:HTML], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:JSON], Mime::Type[:MULTIPART_FORM]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML] + expect = [Mime::Type[:HTML], Mime::Type[:JSON], Mime::Type[:MULTIPART_FORM], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star" do accept = "text/*" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON] + expect = [Mime::Type[:HTML], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:JSON]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse application with trailing star" do accept = "application/*" - expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF, Mime::ZIP] + expect = [Mime::Type[:HTML], Mime::Type[:JS], Mime::Type[:XML], Mime::Type[:RSS], Mime::Type[:ATOM], Mime::Type[:YAML], Mime::Type[:URL_ENCODED_FORM], Mime::Type[:JSON], Mime::Type[:PDF], Mime::Type[:ZIP]] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse without q" do accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" - expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL] + expect = [Mime::Type[:HTML], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:PNG], Mime::Type[:TEXT], Mime::Type[:PDF], Mime::Type[:ALL]] assert_equal expect, Mime::Type.parse(accept) end test "parse with q" do accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,application/pdf,*/*; q=0.2" - expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PDF, Mime::TEXT, Mime::YAML, Mime::ALL] + expect = [Mime::Type[:HTML], Mime::Type[:XML], Mime::Type[:PNG], Mime::Type[:PDF], Mime::Type[:TEXT], Mime::Type[:YAML], Mime::Type[:ALL]] assert_equal expect, Mime::Type.parse(accept) end test "parse single media range with q" do accept = "text/html;q=0.9" - expect = [Mime::HTML] + expect = [Mime::Type[:HTML]] assert_equal expect, Mime::Type.parse(accept) end test "parse arbitrary media type parameters" do accept = 'multipart/form-data; boundary="simple boundary"' - expect = [Mime::MULTIPART_FORM] + expect = [Mime::Type[:MULTIPART_FORM]] assert_equal expect, Mime::Type.parse(accept) end # Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP) test "parse broken acceptlines" do accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5" - expect = [Mime::HTML, Mime::XML, "image/*", Mime::TEXT, Mime::ALL] + expect = [Mime::Type[:HTML], Mime::Type[:XML], "image/*", Mime::Type[:TEXT], Mime::Type[:ALL]] assert_equal expect, Mime::Type.parse(accept).collect(&:to_s) end @@ -90,16 +89,15 @@ class MimeTypeTest < ActiveSupport::TestCase # (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1) test "parse other broken acceptlines" do accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" - expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL] + expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::Type[:ALL]] assert_equal expect, Mime::Type.parse(accept).collect(&:to_s) end test "custom type" do begin - Mime::Type.register("image/foo", :foo) - assert_nothing_raised do - assert_equal Mime::FOO, Mime::SET.last - end + type = Mime::Type.register("image/foo", :foo) + assert_equal Mime::Type[:FOO], type + assert Mime::Type.registered?(:FOO) ensure Mime::Type.unregister(:FOO) end @@ -109,7 +107,7 @@ class MimeTypeTest < ActiveSupport::TestCase begin Mime::Type.register "text/foobar", :foobar, ["text/foo", "text/bar"] %w[text/foobar text/foo text/bar].each do |type| - assert_equal Mime::FOOBAR, type + assert_equal Mime::Type[:FOOBAR], type end ensure Mime::Type.unregister(:FOOBAR) @@ -124,7 +122,7 @@ class MimeTypeTest < ActiveSupport::TestCase end Mime::Type.register("text/foo", :foo) - assert_equal [Mime::FOO], registered_mimes + assert_equal [Mime::Type[:FOO]], registered_mimes ensure Mime::Type.unregister(:FOO) end @@ -134,7 +132,7 @@ class MimeTypeTest < ActiveSupport::TestCase begin Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"] %w[foobar foo bar].each do |extension| - assert_equal Mime::FOOBAR, Mime::EXTENSION_LOOKUP[extension] + assert_equal Mime::Type[:FOOBAR], Mime::EXTENSION_LOOKUP[extension] end ensure Mime::Type.unregister(:FOOBAR) @@ -144,15 +142,15 @@ class MimeTypeTest < ActiveSupport::TestCase test "register alias" do begin Mime::Type.register_alias "application/xhtml+xml", :foobar - assert_equal Mime::HTML, Mime::EXTENSION_LOOKUP['foobar'] + assert_equal Mime::Type[:HTML], Mime::EXTENSION_LOOKUP['foobar'] ensure Mime::Type.unregister(:FOOBAR) end end test "type should be equal to symbol" do - assert_equal Mime::HTML, 'application/xhtml+xml' - assert_equal Mime::HTML, :html + assert_equal Mime::Type[:HTML], 'application/xhtml+xml' + assert_equal Mime::Type[:HTML], :html end test "type convenience methods" do @@ -160,44 +158,51 @@ class MimeTypeTest < ActiveSupport::TestCase types = Mime::SET.symbols.uniq - [:all, :iphone] # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE - types.delete_if { |type| !Mime.const_defined?(type.upcase) } - + types.delete_if { |type| !Mime::Type.registered?(type.upcase) } types.each do |type| - mime = Mime.const_get(type.upcase) + mime = Mime::Type[type.upcase] assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?" - assert mime.send("#{type}?"), "#{mime.inspect} is not #{type}?" + assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?" invalid_types = types - [type] - invalid_types.delete(:html) if Mime::Type.html_types.include?(type) - invalid_types.each { |other_type| assert !mime.send("#{other_type}?"), "#{mime.inspect} is #{other_type}?" } + invalid_types.delete(:html) + invalid_types.each { |other_type| + assert_not_equal mime.symbol, other_type, "#{mime.inspect} is #{other_type}?" + } end end test "mime all is html" do - assert Mime::ALL.all?, "Mime::ALL is not all?" - assert Mime::ALL.html?, "Mime::ALL is not html?" + assert Mime::Type[:ALL].all?, "Mime::ALL is not all?" + assert Mime::Type[:ALL].html?, "Mime::ALL is not html?" + end + + test "deprecated lookup" do + assert_deprecated do + assert Mime::ALL.all?, "Mime::ALL is not all?" + end end test "verifiable mime types" do all_types = Mime::SET.symbols all_types.uniq! # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE - all_types.delete_if { |type| !Mime.const_defined?(type.upcase) } + all_types.delete_if { |type| !Mime::Type.registered?(type.upcase) } end test "references gives preference to symbols before strings" do - assert_equal :html, Mime::HTML.ref + assert_equal :html, Mime::Type[:HTML].ref another = Mime::Type.lookup("foo/bar") assert_nil another.to_sym assert_equal "foo/bar", another.ref end test "regexp matcher" do - assert Mime::JS =~ "text/javascript" - assert Mime::JS =~ "application/javascript" - assert Mime::JS !~ "text/html" - assert !(Mime::JS !~ "text/javascript") - assert !(Mime::JS !~ "application/javascript") - assert Mime::HTML =~ 'application/xhtml+xml' + assert Mime::Type[:JS] =~ "text/javascript" + assert Mime::Type[:JS] =~ "application/javascript" + assert Mime::Type[:JS] !~ "text/html" + assert !(Mime::Type[:JS] !~ "text/javascript") + assert !(Mime::Type[:JS] !~ "application/javascript") + assert Mime::Type[:HTML] =~ 'application/xhtml+xml' end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 258d097b7c..40866595ed 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -750,33 +750,33 @@ class RequestFormat < BaseRequestTest test "xml format" do request = stub_request assert_called(request, :parameters, times: 2, returns: {format: :xml}) do - assert_equal Mime::XML, request.format + assert_equal Mime::Type[:XML], request.format end end test "xhtml format" do request = stub_request assert_called(request, :parameters, times: 2, returns: {format: :xhtml}) do - assert_equal Mime::HTML, request.format + assert_equal Mime::Type[:HTML], request.format end end test "txt format" do request = stub_request assert_called(request, :parameters, times: 2, returns: {format: :txt}) do - assert_equal Mime::TEXT, request.format + assert_equal Mime::Type[:TEXT], request.format end end test "XMLHttpRequest" do request = stub_request( 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', - 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",") + 'HTTP_ACCEPT' => [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], "text/xml", Mime::Type[:ALL]].join(",") ) assert_called(request, :parameters, times: 1, returns: {}) do assert request.xhr? - assert_equal Mime::JS, request.format + assert_equal Mime::Type[:JS], request.format end end @@ -796,29 +796,29 @@ class RequestFormat < BaseRequestTest test "formats text/html with accept header" do request = stub_request 'HTTP_ACCEPT' => 'text/html' - assert_equal [Mime::HTML], request.formats + assert_equal [Mime::Type[:HTML]], request.formats end test "formats blank with accept header" do request = stub_request 'HTTP_ACCEPT' => '' - assert_equal [Mime::HTML], request.formats + assert_equal [Mime::Type[:HTML]], request.formats end test "formats XMLHttpRequest with accept header" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" - assert_equal [Mime::JS], request.formats + assert_equal [Mime::Type[:JS]], request.formats end test "formats application/xml with accept header" do request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest") - assert_equal [Mime::XML], request.formats + assert_equal [Mime::Type[:XML]], request.formats end test "formats format:text with accept header" do request = stub_request assert_called(request, :parameters, times: 2, returns: {format: :txt}) do - assert_equal [Mime::TEXT], request.formats + assert_equal [Mime::Type[:TEXT]], request.formats end end @@ -848,7 +848,7 @@ class RequestFormat < BaseRequestTest test "formats with xhr request" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [Mime::JS], request.formats + assert_equal [Mime::Type[:JS]], request.formats end end @@ -859,35 +859,35 @@ class RequestFormat < BaseRequestTest begin request = stub_request 'HTTP_ACCEPT' => 'application/xml' assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime::HTML ], request.formats + assert_equal [ Mime::Type[:HTML] ], request.formats end request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy' assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime::HTML ], request.formats + assert_equal [ Mime::Type[:HTML] ], request.formats end request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1' assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime::HTML ], request.formats + assert_equal [ Mime::Type[:HTML] ], request.formats end request = stub_request 'HTTP_ACCEPT' => 'application/jxw' assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime::HTML ], request.formats + assert_equal [ Mime::Type[:HTML] ], request.formats end request = stub_request 'HTTP_ACCEPT' => 'application/xml', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" assert_called(request, :parameters, times: 1, returns: {}) do - assert_equal [ Mime::JS ], request.formats + assert_equal [ Mime::Type[:JS] ], request.formats end request = stub_request 'HTTP_ACCEPT' => 'application/xml', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" assert_called(request, :parameters, times: 2, returns: {format: :json}) do - assert_equal [ Mime::JSON ], request.formats + assert_equal [ Mime::Type[:JSON] ], request.formats end ensure ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header @@ -897,7 +897,7 @@ end class RequestMimeType < BaseRequestTest test "content type" do - assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type + assert_equal Mime::Type[:HTML], stub_request('CONTENT_TYPE' => 'text/html').content_mime_type end test "no content type" do @@ -905,11 +905,11 @@ class RequestMimeType < BaseRequestTest end test "content type is XML" do - assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type + assert_equal Mime::Type[:XML], stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type end test "content type with charset" do - assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type + assert_equal Mime::Type[:XML], stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type end test "user agent" do @@ -922,9 +922,9 @@ class RequestMimeType < BaseRequestTest 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" ) - assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON]) - assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML]) - assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL]) + assert_equal nil, request.negotiate_mime([Mime::Type[:XML], Mime::Type[:JSON]]) + assert_equal Mime::Type[:HTML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:HTML]]) + assert_equal Mime::Type[:HTML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:ALL]]) end test "negotiate_mime with content_type" do @@ -933,7 +933,7 @@ class RequestMimeType < BaseRequestTest 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" ) - assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV]) + assert_equal Mime::Type[:XML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:CSV]]) end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 780e7dc3e2..85cdcda655 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -4,7 +4,7 @@ require 'rack/content_length' class ResponseTest < ActiveSupport::TestCase def setup - @response = ActionDispatch::Response.new + @response = ActionDispatch::Response.create end def test_can_wait_until_commit @@ -49,6 +49,11 @@ class ResponseTest < ActiveSupport::TestCase assert_equal 'utf-8', @response.charset end + def test_setting_content_type_header_impacts_content_type_method + @response.headers['Content-Type'] = "application/aaron" + assert_equal 'application/aaron', @response.content_type + end + test "simple output" do @response.body = "Hello, World!" @@ -67,6 +72,13 @@ class ResponseTest < ActiveSupport::TestCase assert_equal 200, ActionDispatch::Response.new('200 OK').status end + def test_only_set_charset_still_defaults_to_text_html + response = ActionDispatch::Response.new + response.charset = "utf-16" + _,headers,_ = response.to_a + assert_equal "text/html; charset=utf-16", headers['Content-Type'] + end + test "utf8 output" do @response.body = [1090, 1077, 1089, 1090].pack("U*") @@ -137,12 +149,19 @@ class ResponseTest < ActiveSupport::TestCase status, headers, body = @response.to_a assert_equal "user_name=david; path=/", headers["Set-Cookie"] assert_equal({"user_name" => "david"}, @response.cookies) + end + test "multiple cookies" do + @response.set_cookie("user_name", :value => "david", :path => "/") @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5)) status, headers, body = @response.to_a assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"] assert_equal({"login" => "foo&bar", "user_name" => "david"}, @response.cookies) + end + test "delete cookies" do + @response.set_cookie("user_name", :value => "david", :path => "/") + @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5)) @response.delete_cookie("login") status, headers, body = @response.to_a assert_equal({"user_name" => "david", "login" => nil}, @response.cookies) @@ -166,13 +185,13 @@ class ResponseTest < ActiveSupport::TestCase test "read charset and content type" do resp = ActionDispatch::Response.new.tap { |response| response.charset = 'utf-16' - response.content_type = Mime::XML + response.content_type = Mime::Type[:XML] response.body = 'Hello' } resp.to_a assert_equal('utf-16', resp.charset) - assert_equal(Mime::XML, resp.content_type) + assert_equal(Mime::Type[:XML], resp.content_type) assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type']) end @@ -198,7 +217,7 @@ class ResponseTest < ActiveSupport::TestCase 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1;' } - resp = ActionDispatch::Response.new.tap { |response| + resp = ActionDispatch::Response.create.tap { |response| response.body = 'Hello' } resp.to_a @@ -217,7 +236,7 @@ class ResponseTest < ActiveSupport::TestCase ActionDispatch::Response.default_headers = { 'X-XX-XXXX' => 'Here is my phone number' } - resp = ActionDispatch::Response.new.tap { |response| + resp = ActionDispatch::Response.create.tap { |response| response.body = 'Hello' } resp.to_a @@ -305,7 +324,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| resp.charset = 'utf-16' - resp.content_type = Mime::XML + resp.content_type = Mime::Type[:XML] resp.body = 'Hello' }.to_a } @@ -314,7 +333,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('utf-16', @response.charset) - assert_equal(Mime::XML, @response.content_type) + assert_equal(Mime::Type[:XML], @response.content_type) assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type']) end @@ -330,7 +349,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest assert_response :success assert_equal('utf-16', @response.charset) - assert_equal(Mime::XML, @response.content_type) + assert_equal(Mime::Type[:XML], @response.content_type) assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type']) end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index d512dae4e7..6a3af32946 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 require 'abstract_unit' module ActionDispatch diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index f97ca88b87..9ea250643a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,7 @@ +* `url_for` does not modify its arguments when generating polymorphic URLs. + + *Bernerd Schaefer* + * `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option to handle placement of delimiter, to support currency formats like INR diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 6e8c7f8203..7716955fd9 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -1,9 +1,9 @@ -require 'thread_safe' +require 'concurrent' require 'action_view/path_set' module ActionView class DependencyTracker # :nodoc: - @trackers = ThreadSafe::Cache.new + @trackers = Concurrent::Map.new def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 9a8a4feb2e..12e9723a02 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -1,11 +1,11 @@ -require 'thread_safe' +require 'concurrent' require 'action_view/dependency_tracker' require 'monitor' module ActionView class Digestor cattr_reader(:cache) - @@cache = ThreadSafe::Cache.new + @@cache = Concurrent::Map.new @@digest_monitor = Monitor.new class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: @@ -28,7 +28,7 @@ module ActionView cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') # this is a correctly done double-checked locking idiom - # (ThreadSafe::Cache's lookups have volatile semantics) + # (Concurrent::Map's lookups have volatile semantics) @@cache[cache_key] || @@digest_monitor.synchronize do @@cache.fetch(cache_key) do # re-check under lock compute_and_store_digest(cache_key, options) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 3a9acafaa2..5a9a5d6d18 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -849,8 +849,8 @@ module ActionView # file_field(:user, :avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> # - # file_field(:post, :image, :multiple => true) - # # => <input type="file" id="post_image" name="post[image]" multiple="true" /> + # file_field(:post, :image, multiple: true) + # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" /> # # file_field(:post, :attached, accept: 'text/html') # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> @@ -1880,7 +1880,7 @@ module ActionView # create: "Add %{model}" # # ==== Examples - # button("Create a post") + # button("Create post") # # => <button name='button' type='submit'>Create post</button> # # button do diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 13effa592d..d7182d1fac 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -1,4 +1,3 @@ - require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/string/output_safety' require 'active_support/number_helper' @@ -140,7 +139,7 @@ module ActionView # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% # number_to_percentage(1000, locale: :fr) # => 1 000,000% # number_to_percentage("98a") # => 98a% - # number_to_percentage(100, format: "%n %") # => 100 % + # number_to_percentage(100, format: "%n %") # => 100.000 % # # number_to_percentage("98a", raise: true) # => InvalidNumberError def number_to_percentage(number, options = {}) diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 0615bd2e0d..dde1ef22ac 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -88,7 +88,14 @@ module ActionView raise e if raise_error keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}") + title = "translation missing: #{keys.join('.')}" + + interpolations = options.except(:default) + if interpolations.any? + title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ') + end + + content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title) end end alias :t :translate diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index fba9a44cb1..ec6edfaaa3 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/module/attribute_accessors' require 'action_view/template/resolver' @@ -20,7 +20,7 @@ module ActionView mattr_accessor :registered_details self.registered_details = [] - def self.register_detail(name, options = {}, &block) + def self.register_detail(name, &block) self.registered_details << name initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } @@ -55,14 +55,14 @@ module ActionView end register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } register_detail(:variants) { [] } - register_detail(:handlers){ Template::Handlers.extensions } + register_detail(:handlers) { Template::Handlers.extensions } class DetailsKey #:nodoc: alias :eql? :equal? alias :object_hash :hash attr_reader :hash - @details_keys = ThreadSafe::Cache.new + @details_keys = Concurrent::Map.new def self.get(details) if details[:formats] diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 6c6e69101b..4b44eb5520 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -11,7 +11,7 @@ module ActionView # <%= post.body %> # <% end %> # - # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML + # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML # is: # # <div id="new_post" class="post"> diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 1f9e960488..39c8658ffe 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,5 @@ require 'action_view/renderer/partial_renderer/collection_caching' -require 'thread_safe' +require 'concurrent' module ActionView class PartialIteration @@ -283,8 +283,8 @@ module ActionView class PartialRenderer < AbstractRenderer include CollectionCaching - PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| - h[k] = ThreadSafe::Cache.new + PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k| + h[k] = Concurrent::Map.new end def initialize(*) diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 20d6b9a64c..b4cbc80bd5 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -105,10 +105,11 @@ module ActionView when :back _back_url when Array + components = options.dup if _generate_paths_by_default - polymorphic_path(options, options.extract_options!) + polymorphic_path(components, components.extract_options!) else - polymorphic_url(options, options.extract_options!) + polymorphic_url(components, components.extract_options!) end else method = _generate_paths_by_default ? :path : :url diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 28967f40a6..7859c58b43 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -3,7 +3,7 @@ require "active_support/core_ext/class" require "active_support/core_ext/module/attribute_accessors" require "action_view/template" require "thread" -require "thread_safe" +require "concurrent" module ActionView # = Action View Resolver @@ -35,7 +35,7 @@ module ActionView # Threadsafe template cache class Cache #:nodoc: - class SmallCache < ThreadSafe::Cache + class SmallCache < Concurrent::Map def initialize(options = {}) super(options.merge(:initial_capacity => 2)) end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 12870acaa6..2354e91822 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -151,7 +151,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase 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 diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb index f8387b27b0..933456ce9d 100644 --- a/actionview/test/actionpack/controller/capture_test.rb +++ b/actionview/test/actionpack/controller/capture_test.rb @@ -54,7 +54,7 @@ class CaptureTest < ActionController::TestCase assert_equal expected_content_for_output, @response.body end - def test_should_concatentate_content_for + def test_should_concatenate_content_for get :content_for_concatenated assert_equal expected_content_for_output, @response.body end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 8d048ddbcb..27150c7d5f 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1122,7 +1122,7 @@ class RenderTest < ActionController::TestCase assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body end - def test_overwritting_rendering_relative_file_with_extension + def test_overwriting_rendering_relative_file_with_extension get :hello_world_from_rxml_using_template assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index 672b4747ec..3ece9e50cd 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'action_view/dependency_tracker' diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index aef137935a..77c660d450 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -366,7 +366,7 @@ class FormHelperTest < ActionView::TestCase ) end - def test_label_with_to_model_and_overriden_model_name + def test_label_with_to_model_and_overridden_model_name with_locale :label do assert_dom_equal( %{<label for="post_delegator_title">Delegate model_name title</label>}, diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 14dafd7d63..1184cf7da8 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -27,7 +27,7 @@ class LookupContextTest < ActiveSupport::TestCase end test "normalizes details on initialization" do - assert_equal Mime::SET, @lookup_context.formats + assert_equal Mime::SET.to_a, @lookup_context.formats assert_equal :en, @lookup_context.locale end @@ -48,11 +48,11 @@ class LookupContextTest < ActiveSupport::TestCase test "handles */* formats" do @lookup_context.formats = ["*/*"] - assert_equal Mime::SET, @lookup_context.formats + assert_equal Mime::SET.to_a, @lookup_context.formats end test "handles explicitly defined */* formats fallback to :js" do - @lookup_context.formats = [:js, Mime::ALL] + @lookup_context.formats = [:js, Mime::Type[:ALL]] assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats end diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index 631d45586b..ace3e950b8 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require "abstract_unit" class NumberHelperTest < ActionView::TestCase diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index 749d0dd7fd..261576bead 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -60,6 +60,12 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translate(:"translations.missing").html_safe? end + def test_returns_missing_translation_message_with_unescaped_interpolation + expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &quot; onclick=&quot;alert()&quot;">Missing</span>' + assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"}) + assert translate(:"translations.missing").html_safe? + end + def test_raises_missing_translation_message_with_raise_config_option ActionView::Base.raise_on_missing_translations = true diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 027b72b354..50b7865f88 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -785,6 +785,13 @@ class SessionsController < ActionController::Base @session = Session.new(params[:id]) render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>" end + + def edit + @workshop = Workshop.new(params[:workshop_id]) + @session = Session.new(params[:id]) + @url = [@workshop, @session, format: params[:format]] + render inline: "<%= url_for(@url) %>\n<%= link_to('Session', @url) %>" + end end class PolymorphicControllerTest < ActionController::TestCase @@ -815,4 +822,11 @@ class PolymorphicControllerTest < ActionController::TestCase get :show, params: { workshop_id: 1, id: 1 } assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body end + + def test_existing_nested_resource_with_params + @controller = SessionsController.new + + get :edit, params: { workshop_id: 1, id: 1, format: "json" } + assert_equal %{/workshops/1/sessions/1.json\n<a href="/workshops/1/sessions/1.json">Session</a>}, @response.body + end end diff --git a/activejob/lib/active_job/async_job.rb b/activejob/lib/active_job/async_job.rb index 7fcffc4c24..6c1c070994 100644 --- a/activejob/lib/active_job/async_job.rb +++ b/activejob/lib/active_job/async_job.rb @@ -1,5 +1,4 @@ require 'concurrent' -require 'thread_safe' module ActiveJob # == Active Job Async Job @@ -31,7 +30,7 @@ module ActiveJob fallback_policy: :caller_runs # shouldn't matter -- 0 max queue }.freeze - QUEUES = ThreadSafe::Cache.new do |hash, queue_name| #:nodoc: + QUEUES = Concurrent::Map.new do |hash, queue_name| #:nodoc: hash.compute_if_absent(queue_name) { ActiveJob::AsyncJob.create_thread_pool } end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 54774db601..d72e1bdfce 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/filters' require 'active_support/tagged_logging' require 'active_support/logger' @@ -87,12 +88,25 @@ module ActiveJob def args_info(job) if job.arguments.any? ' with arguments: ' + - job.arguments.map { |arg| arg.try(:to_global_id).try(:to_s) || arg.inspect }.join(', ') + job.arguments.map { |arg| format(arg).inspect }.join(', ') else '' end end + def format(arg) + case arg + when Hash + arg.transform_values { |value| format(value) } + when Array + arg.map { |value| format(value) } + when GlobalID::Identification + arg.to_global_id rescue arg + else + arg + end + end + def scheduled_at(event) Time.at(event.payload[:job].scheduled_at).utc end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index b18be553ec..820e9112de 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -74,6 +74,14 @@ class LoggingTest < ActiveSupport::TestCase assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) end + def test_globalid_nested_parameter_logging + person = Person.new(123) + LoggingJob.perform_later(person: person) + assert_match(%r{Enqueued.*gid://aj/Person/123}, @logger.messages) + assert_match(%r{Dummy, here is it: .*#<Person:.*>}, @logger.messages) + assert_match(%r{Performing.*gid://aj/Person/123}, @logger.messages) + end + def test_enqueue_job_logging HelloJob.perform_later "Cristian" assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb index ee816e1dd5..616454a4b6 100644 --- a/activejob/test/cases/test_case_test.rb +++ b/activejob/test/cases/test_case_test.rb @@ -9,7 +9,7 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase # the `class_attribute` inheritance class TestClassAttributeInheritanceJob < ActiveJob::Base def self.queue_adapter=(*) - raise 'Attemping to break `class_attribute` inheritance, bad!' + raise 'Attempting to break `class_attribute` inheritance, bad!' end end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 55f690bda8..7e86415f48 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -11,6 +11,7 @@ GlobalID.app = 'aj' if ENV['AJ_INTEGRATION_TESTS'] require 'support/integration/helper' else + ActiveJob::Base.logger = Logger.new(nil) require "adapters/#{@adapter}" end diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index d9938600f2..9aa07bcb52 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -58,7 +58,7 @@ module SidekiqJobsManager timeout: 1, }) Sidekiq.average_scheduled_poll_interval = 0.5 - Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 + Sidekiq.options[:poll_interval_average] = 1 begin sidekiq.run continue_write.puts "started" diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 4ffdb8cffa..0c062a025e 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -1,8 +1,9 @@ if ENV['AJ_ADAPTER'] == 'delayed_job' generate "delayed_job:active_record", "--quiet" - rake("db:migrate") end +rake("db:migrate") + initializer 'activejob.rb', <<-CODE require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" JobsManager.current_manager.setup diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index 8c2e5a86c2..4a1b0bfbcb 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -1,4 +1,4 @@ -puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" +puts "\n\n*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n" ENV["RAILS_ENV"] = "test" ActiveJob::Base.queue_name_prefix = nil diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 54fe5794e1..bf0120122d 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,21 @@ +* Validate multiple contexts on `valid?` and `invalid?` at once. + + Example: + + class Person + include ActiveModel::Validations + + attr_reader :name, :title + validates_presence_of :name, on: :create + validates_presence_of :title, on: :update + end + + person = Person.new + person.valid?([:create, :update]) # => false + person.errors.messages # => {:name=>["can't be blank"], :title=>["can't be blank"]} + + *Dmitry Polushkin* + * Add case_sensitive option for confirmation validator in models. *Akshat Sharma* diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 77e4ce3afe..1963a3fc4e 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent' require 'mutex_m' module ActiveModel @@ -350,7 +350,7 @@ module ActiveModel # significantly (in our case our test suite finishes 10% faster with # this cache). def attribute_method_matchers_cache #:nodoc: - @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4) + @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4) end def attribute_method_matchers_matching(method_name) #:nodoc: diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 2cf39b68fb..0d6a3dc52d 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -103,6 +103,7 @@ module ActiveModel def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 29e0c977ce..4726a68f69 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/deep_dup' diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb new file mode 100644 index 0000000000..f8ca7d0512 --- /dev/null +++ b/activemodel/lib/active_model/type.rb @@ -0,0 +1,57 @@ +require 'active_model/type/helpers' +require 'active_model/type/value' + +require 'active_model/type/big_integer' +require 'active_model/type/binary' +require 'active_model/type/boolean' +require 'active_model/type/date' +require 'active_model/type/date_time' +require 'active_model/type/decimal' +require 'active_model/type/decimal_without_scale' +require 'active_model/type/float' +require 'active_model/type/integer' +require 'active_model/type/string' +require 'active_model/type/text' +require 'active_model/type/time' +require 'active_model/type/unsigned_integer' + +require 'active_model/type/registry' + +module ActiveModel + module Type + @registry = Registry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, **kwargs) # :nodoc: + registry.lookup(*args, **kwargs) + end + end + + register(:big_integer, Type::BigInteger) + register(:binary, Type::Binary) + register(:boolean, Type::Boolean) + register(:date, Type::Date) + register(:date_time, Type::DateTime) + register(:decimal, Type::Decimal) + register(:float, Type::Float) + register(:integer, Type::Integer) + register(:string, Type::String) + register(:text, Type::Text) + register(:time, Type::Time) + end +end diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb index 0c72d8914f..4168cbfce7 100644 --- a/activerecord/lib/active_record/type/big_integer.rb +++ b/activemodel/lib/active_model/type/big_integer.rb @@ -1,6 +1,6 @@ -require 'active_record/type/integer' +require 'active_model/type/integer' -module ActiveRecord +module ActiveModel module Type class BigInteger < Integer # :nodoc: private diff --git a/activerecord/lib/active_record/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index 0baf8c63ad..a0cc45b4c3 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Binary < Value # :nodoc: def type diff --git a/activerecord/lib/active_record/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb index f6a75512fd..c1bce98c87 100644 --- a/activerecord/lib/active_record/type/boolean.rb +++ b/activemodel/lib/active_model/type/boolean.rb @@ -1,6 +1,8 @@ -module ActiveRecord +module ActiveModel module Type class Boolean < Value # :nodoc: + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set + def type :boolean end @@ -11,7 +13,7 @@ module ActiveRecord if value == '' nil else - !ConnectionAdapters::Column::FALSE_VALUES.include?(value) + !FALSE_VALUES.include?(value) end end end diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb new file mode 100644 index 0000000000..f74243a22c --- /dev/null +++ b/activemodel/lib/active_model/type/date.rb @@ -0,0 +1,50 @@ +module ActiveModel + module Type + class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + + def type + :date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + def fast_string_to_date(string) + if string =~ ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end + end + end +end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb new file mode 100644 index 0000000000..2f2df4320f --- /dev/null +++ b/activemodel/lib/active_model/type/date_time.rb @@ -0,0 +1,44 @@ +module ActiveModel + module Type + class DateTime < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) + + def type + :datetime + end + + private + + def cast_value(value) + return apply_seconds_precision(value) unless value.is_a?(::String) + return if value.empty? + + fast_string_to_time(value) || fallback_string_to_time(value) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index f5b145230d..d19d8baada 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -1,4 +1,6 @@ -module ActiveRecord +require "bigdecimal/util" + +module ActiveModel module Type class Decimal < Value # :nodoc: include Helpers::Numeric diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb index ff5559e300..129baa0c10 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activemodel/lib/active_model/type/decimal_without_scale.rb @@ -1,6 +1,6 @@ -require 'active_record/type/big_integer' +require 'active_model/type/big_integer' -module ActiveRecord +module ActiveModel module Type class DecimalWithoutScale < BigInteger # :nodoc: def type diff --git a/activerecord/lib/active_record/type/float.rb b/activemodel/lib/active_model/type/float.rb index d88482b85d..0f925bc7e1 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activemodel/lib/active_model/type/float.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Float < Value # :nodoc: include Helpers::Numeric diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb new file mode 100644 index 0000000000..a805a359ab --- /dev/null +++ b/activemodel/lib/active_model/type/helpers.rb @@ -0,0 +1,4 @@ +require 'active_model/type/helpers/accepts_multiparameter_time' +require 'active_model/type/helpers/numeric' +require 'active_model/type/helpers/mutable' +require 'active_model/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb index be571fc1c7..fa1ccd057e 100644 --- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers class AcceptsMultiparameterTime < Module # :nodoc: @@ -17,10 +17,7 @@ module ActiveRecord end return unless values_hash[1] && values_hash[2] && values_hash[3] values = values_hash.sort.map(&:last) - ::Time.send( - ActiveRecord::Base.default_timezone, - *values - ) + ::Time.send(default_timezone, *values) end private :value_from_multiparameter_assignment end diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb index 88a9099277..4dddbe4e5e 100644 --- a/activerecord/lib/active_record/type/helpers/mutable.rb +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers module Mutable # :nodoc: diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index a755a02a59..c883010506 100644 --- a/activerecord/lib/active_record/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type module Helpers module Numeric # :nodoc: diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 7eb41557cb..63993c0d93 100644 --- a/activerecord/lib/active_record/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,16 +1,14 @@ -module ActiveRecord +require "active_support/core_ext/time/zones" + +module ActiveModel module Type module Helpers module TimeValue # :nodoc: def serialize(value) - if precision && value.respond_to?(:usec) - number_of_insignificant_digits = 6 - precision - round_power = 10 ** number_of_insignificant_digits - value = value.change(usec: value.usec / round_power * round_power) - end + value = apply_seconds_precision(value) if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + zone_conversion_method = is_utc? ? :getutc : :getlocal if value.respond_to?(zone_conversion_method) value = value.send(zone_conversion_method) @@ -20,6 +18,25 @@ module ActiveRecord value end + def is_utc? + ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC' + end + + def default_timezone + if is_utc? + :utc + else + :local + end + end + + def apply_seconds_precision(value) + return value unless precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value.change(usec: value.usec / round_power * round_power) + end + def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end @@ -39,15 +56,17 @@ module ActiveRecord return unless time time -= offset - Base.default_timezone == :utc ? time : time.getlocal + is_utc? ? time : time.getlocal else - ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil end end + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + # Doesn't handle time zones. def fast_string_to_time(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + if string =~ ISO_DATETIME microsec = ($7.to_r * 1_000_000).to_i new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end diff --git a/activerecord/lib/active_record/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index c5040c6d3b..2f73ede009 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Integer < Value # :nodoc: include Helpers::Numeric diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb new file mode 100644 index 0000000000..adc88eb624 --- /dev/null +++ b/activemodel/lib/active_model/type/registry.rb @@ -0,0 +1,64 @@ +module ActiveModel + # :stopdoc: + module Type + class Registry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << registration_klass.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = find_registration(symbol, *args) + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + protected + + attr_reader :registrations + + private + + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations.find { |r| r.matches?(symbol, *args) } + end + end + + class Registration + # Options must be taken because of https://bugs.ruby-lang.org/issues/10856 + def initialize(name, block, **) + @name = name + @block = block + end + + def call(_registry, *args, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name + end + + protected + + attr_reader :name, :block + end + end + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/string.rb b/activemodel/lib/active_model/type/string.rb index 2662b7e874..fd1630c751 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class String < Value # :nodoc: def type diff --git a/activerecord/lib/active_record/type/text.rb b/activemodel/lib/active_model/type/text.rb index 26f980f060..1ad04daba4 100644 --- a/activerecord/lib/active_record/type/text.rb +++ b/activemodel/lib/active_model/type/text.rb @@ -1,6 +1,6 @@ -require 'active_record/type/string' +require 'active_model/type/string' -module ActiveRecord +module ActiveModel module Type class Text < String # :nodoc: def type diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb new file mode 100644 index 0000000000..7101bad566 --- /dev/null +++ b/activemodel/lib/active_model/type/time.rb @@ -0,0 +1,42 @@ +module ActiveModel + module Type + class Time < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) + + def type + :time + end + + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb index ed3e527483..3f49f9f5f7 100644 --- a/activerecord/lib/active_record/type/unsigned_integer.rb +++ b/activemodel/lib/active_model/type/unsigned_integer.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class UnsignedInteger < Integer # :nodoc: private diff --git a/activerecord/lib/active_record/type/value.rb b/activemodel/lib/active_model/type/value.rb index 6b9d147ecc..c7d1197d69 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activemodel/lib/active_model/type/value.rb @@ -1,4 +1,4 @@ -module ActiveRecord +module ActiveModel module Type class Value attr_reader :precision, :scale, :limit diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 5f1dde4aa3..f23c920d87 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -162,7 +162,7 @@ module ActiveModel options = options.dup options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - Array(options[:on]).include?(o.validation_context) + !(Array(options[:on]) & Array(o.validation_context)).empty? } end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..52111e5442 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -23,6 +23,7 @@ module ActiveModel included do include ActiveSupport::Callbacks define_callbacks :validation, + terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index bb2f0bd064..8f8ade90bb 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -9,7 +9,7 @@ module ActiveModel def validate_each(record, attribute, value) if (confirmed = record.send("#{attribute}_confirmation")) - unless confimation_value_equal?(record, attribute, value, confirmed) + unless confirmation_value_equal?(record, attribute, value, confirmed) human_attribute_name = record.class.human_attribute_name(attribute) record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name)) end @@ -27,7 +27,7 @@ module ActiveModel end.compact) end - def confimation_value_equal?(record, attribute, value, confirmed) + def confirmation_value_equal?(record, attribute, value, confirmed) if !options[:case_sensitive] && value.is_a?(String) value.casecmp(confirmed) == 0 else diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 64a85e01eb..3336691841 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -70,7 +70,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase end end - test "an ArgumentError is raised if a non-hash-like obejct is passed" do + test "an ArgumentError is raised if a non-hash-like object is passed" do assert_raises(ArgumentError) do Model.new(1) end diff --git a/activerecord/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index 01ee36b892..353dbf84ad 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "active_model/type" -module ActiveRecord +module ActiveModel module Type - class DecimalTest < ActiveRecord::TestCase + class DecimalTest < ActiveModel::TestCase def test_type_cast_decimal type = Decimal.new assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb new file mode 100644 index 0000000000..dac922db42 --- /dev/null +++ b/activemodel/test/cases/type/integer_test.rb @@ -0,0 +1,108 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + module Type + class IntegerTest < ActiveModel::TestCase + test "simple values" do + type = Type::Integer.new + assert_equal 1, type.cast(1) + assert_equal 1, type.cast('1') + assert_equal 1, type.cast('1ignore') + assert_equal 0, type.cast('bad1') + assert_equal 0, type.cast('bad') + assert_equal 1, type.cast(1.7) + assert_equal 0, type.cast(false) + assert_equal 1, type.cast(true) + assert_nil type.cast(nil) + end + + test "random objects cast to nil" do + type = Type::Integer.new + assert_nil type.cast([1,2]) + assert_nil type.cast({1 => 2}) + assert_nil type.cast(1..2) + end + + test "casting objects without to_i" do + type = Type::Integer.new + assert_nil type.cast(::Object.new) + end + + test "casting nan and infinity" do + type = Type::Integer.new + assert_nil type.cast(::Float::NAN) + assert_nil type.cast(1.0/0.0) + end + + test "casting booleans for database" do + type = Type::Integer.new + assert_equal 1, type.serialize(true) + assert_equal 0, type.serialize(false) + end + + test "changed?" do + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') + assert_not type.changed?(nil, nil, nil) + end + + test "values below int min value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-2147483649) + end + end + + test "values above int max value are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(2147483648) + end + end + + test "very small numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(-9999999999999999999999999999999) + end + end + + test "very large numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.serialize(9999999999999999999999999999999) + end + end + + test "normal numbers are in range" do + type = Integer.new + assert_equal(0, type.serialize(0)) + assert_equal(-1, type.serialize(-1)) + assert_equal(1, type.serialize(1)) + end + + test "int max value is in range" do + assert_equal(2147483647, Integer.new.serialize(2147483647)) + end + + test "int min value is in range" do + assert_equal(-2147483648, Integer.new.serialize(-2147483648)) + end + + test "columns with a larger limit have larger ranges" do + type = Integer.new(limit: 8) + + assert_equal(9223372036854775807, type.serialize(9223372036854775807)) + assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) + assert_raises(::RangeError) do + type.serialize(-9999999999999999999999999999999) + end + assert_raises(::RangeError) do + type.serialize(9999999999999999999999999999999) + end + end + end + end +end diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb new file mode 100644 index 0000000000..2a48998a62 --- /dev/null +++ b/activemodel/test/cases/type/registry_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class RegistryTest < ActiveModel::TestCase + test "a class can be registered for a symbol" do + registry = Type::Registry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) + + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) + end + + test "a block can be registered" do + registry = Type::Registry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + end + registry.register(:bar) do |*args| + [*args, "block for bar"] + end + + assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) + assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) + assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) + end + + test "a reasonable error is given when no type is found" do + registry = Type::Registry.new + + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end + + assert_equal "Unknown type :foo", e.message + end + end +end diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb new file mode 100644 index 0000000000..8ec771ea42 --- /dev/null +++ b/activemodel/test/cases/type/string_test.rb @@ -0,0 +1,20 @@ +require "cases/helper" +require "active_model/type" + +module ActiveModel + class StringTypeTest < ActiveModel::TestCase + test "type casting" do + type = Type::String.new + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.cast(s) + assert_not_same s, type.deserialize(s) + end + end +end diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb index f2c910eade..16301b3ac0 100644 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ b/activemodel/test/cases/type/unsigned_integer_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "active_model/type" -module ActiveRecord +module ActiveModel module Type - class UnsignedIntegerTest < ActiveRecord::TestCase + class UnsignedIntegerTest < ActiveModel::TestCase test "unsigned int max value is in range" do assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) end diff --git a/activemodel/test/cases/types_test.rb b/activemodel/test/cases/types_test.rb new file mode 100644 index 0000000000..f937208580 --- /dev/null +++ b/activemodel/test/cases/types_test.rb @@ -0,0 +1,122 @@ +require "cases/helper" +require "active_model/type" +require "active_support/core_ext/numeric/time" + +module ActiveModel + class TypesTest < ActiveModel::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.cast('').nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast('1') + assert type.cast('t') + assert type.cast('T') + assert type.cast('true') + assert type.cast('TRUE') + assert type.cast('on') + assert type.cast('ON') + assert type.cast(' ') + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast('SOMETHING RANDOM') + + # explicitly check for false vs nil + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.cast("1") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) + end + + def test_string_to_time_with_timezone + ["UTC", "US/Eastern"].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + + private + + def with_timezone_config(default:) + old_zone_default = ::Time.zone_default + ::Time.zone_default = ::Time.find_zone(default) + yield + ensure + ::Time.zone_default = old_zone_default + end + end +end diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 150dce379f..b901a1523e 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -8,6 +8,7 @@ class ValidationsContextTest < ActiveModel::TestCase end ERROR_MESSAGE = "Validation error from validator" + ANOTHER_ERROR_MESSAGE = "Another validation error from validator" class ValidatorThatAddsErrors < ActiveModel::Validator def validate(record) @@ -15,6 +16,12 @@ class ValidationsContextTest < ActiveModel::TestCase end end + class AnotherValidatorThatAddsErrors < ActiveModel::Validator + def validate(record) + record.errors[:base] << ANOTHER_ERROR_MESSAGE + end + end + test "with a class that adds errors on create and validating a new model with no arguments" do Topic.validates_with(ValidatorThatAddsErrors, on: :create) topic = Topic.new @@ -46,4 +53,16 @@ class ValidationsContextTest < ActiveModel::TestCase assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2" assert topic.errors[:base].include?(ERROR_MESSAGE) end + + test "with a class that validating a model for a multiple contexts" do + Topic.validates_with(ValidatorThatAddsErrors, on: :context1) + Topic.validates_with(AnotherValidatorThatAddsErrors, on: :context2) + + topic = Topic.new + assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2" + + assert topic.invalid?([:context1, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2" + assert topic.errors[:base].include?(ERROR_MESSAGE) + assert topic.errors[:base].include?(ANOTHER_ERROR_MESSAGE) + end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 554bec17d6..36fe3e5234 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,87 @@ +* Correctly apply `unscope` when preloading through associations. + + *Jimmy Bourassa* + +* Fixed taking precision into count when assigning a value to timestamp attribute + + Timestamp column can have less precision than ruby timestamp + In result in how big a fraction of a second can be stored in the + database. + + + m = Model.create! + m.created_at.usec == m.reload.created_at.usec + # => false + # due to different precision in Time.now and database column + + If the precision is low enough, (mysql default is 0, so it is always low + enough by default) the value changes when model is reloaded from the + database. This patch fixes that issue ensuring that any timestamp + assigned as an attribute is converted to column precision under the + attribute. + + *Bogdan Gusiev* + +* Introduce `connection.data_sources` and `connection.data_source_exists?`. + These methods determine what relations can be used to back Active Record + models (usually tables and views). + + Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and + `SchemaCache#clear_table_cache!` in favor of their new data source + counterparts. + + *Yves Senn*, *Matthew Draper* + +* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to + mysql commands (like `mysqldump`) is not successful. + + *Steve Mitchell* + +* Ensure `select` quotes aliased attributes, even when using `from`. + + Fixes #21488 + + *Sean Griffin & @johanlunds* + +* MySQL: support `unsigned` numeric data types. + + Example: + + create_table :foos do |t| + t.unsigned_integer :quantity + t.unsigned_bigint :total + t.unsigned_float :percentage + t.unsigned_decimal :price, precision: 10, scale: 2 + end + + The `unsigned: true` option may be used for the primary key: + + create_table :foos, id: :bigint, unsigned: true do |t| + … + end + + *Ryuta Kamizono* + +* Add `#views` and `#view_exists?` methods on connection adapters. + + *Ryuta Kamizono* + +* Correctly dump composite primary key. + + Example: + + create_table :barcodes, primary_key: ["region", "code"] do |t| + t.string :region + t.integer :code + end + + *Ryuta Kamizono* + +* Lookup the attribute name for `restrict_with_error` messages on the + model class that defines the association. + + *kuboon*, *Ronak Jangir* + * Correct query for PostgreSQL 8.2 compatibility. *Ben Murphy*, *Matthew Draper* diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 049c5d2b3b..3eac8cc422 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products", which might look like this: CREATE TABLE products ( - id int(11) NOT NULL auto_increment, + id int NOT NULL auto_increment, name varchar(255), PRIMARY KEY (id) ); diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index f7b50cd25a..a2aea63bdd 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -25,7 +25,7 @@ module ActiveRecord end # Active Record implements aggregation through a macro-like class method called +composed_of+ - # for representing attributes as value objects. It expresses relationships like "Account [is] + # for representing attributes as value objects. It expresses relationships like "Account [is] # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call # to the macro adds a description of how the value objects are created from the attributes of # the entity object (when the entity is initialized either as a new object or from finding an diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 65eb6fba27..cf3e63b4a3 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -446,14 +446,14 @@ module ActiveRecord # The tables for these classes could look something like: # # CREATE TABLE users ( - # id int(11) NOT NULL auto_increment, - # account_id int(11) default NULL, + # id int NOT NULL auto_increment, + # account_id int default NULL, # name varchar default NULL, # PRIMARY KEY (id) # ) # # CREATE TABLE accounts ( - # id int(11) NOT NULL auto_increment, + # id int NOT NULL auto_increment, # name varchar default NULL, # PRIMARY KEY (id) # ) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 29c711082a..79b6b80d84 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -881,7 +881,7 @@ module ActiveRecord !!@association.include?(record) end - def arel + def arel #:nodoc: scope.arel end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 19cdd7f470..38bda0d2a5 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -15,7 +15,7 @@ module ActiveRecord when :restrict_with_error unless empty? - record = klass.human_attribute_name(reflection.name).downcase + record = owner.class.human_attribute_name(reflection.name).downcase message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil if message ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) @@ -50,7 +50,7 @@ module ActiveRecord end def empty? - if has_cached_counter? + if reflection.has_cached_counter? size.zero? else super @@ -73,8 +73,8 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = if has_cached_counter? - owner._read_attribute cached_counter_attribute_name + count = if reflection.has_cached_counter? + owner._read_attribute reflection.counter_cache_column else scope.count end @@ -87,77 +87,26 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - - # Returns whether a counter cache should be used for this association. - # - # The counter_cache option must be given on either the owner or inverse - # association, and the column must be present on the owner. - def has_cached_counter?(reflection = reflection()) - if reflection.options[:counter_cache] || (inverse = inverse_which_updates_counter_cache(reflection)) && inverse.options[:counter_cache] - owner.attribute_present?(cached_counter_attribute_name(reflection)) - end - end - - def cached_counter_attribute_name(reflection = reflection()) - if reflection.options[:counter_cache] - reflection.options[:counter_cache].to_s - else - "#{reflection.name}_count" - end - end - def update_counter(difference, reflection = reflection()) update_counter_in_database(difference, reflection) update_counter_in_memory(difference, reflection) end def update_counter_in_database(difference, reflection = reflection()) - if has_cached_counter?(reflection) - counter = cached_counter_attribute_name(reflection) - owner.class.update_counters(owner.id, counter => difference) + if reflection.has_cached_counter? + owner.class.update_counters(owner.id, reflection.counter_cache_column => difference) end end def update_counter_in_memory(difference, reflection = reflection()) - if counter_must_be_updated_by_has_many?(reflection) - counter = cached_counter_attribute_name(reflection) + if reflection.counter_must_be_updated_by_has_many? + counter = reflection.counter_cache_column + owner[counter] ||= 0 owner[counter] += difference owner.send(:clear_attribute_changes, counter) # eww end end - # This shit is nasty. We need to avoid the following situation: - # - # * An associated record is deleted via record.destroy - # * Hence the callbacks run, and they find a belongs_to on the record with a - # :counter_cache options which points back at our owner. So they update the - # counter cache. - # * In which case, we must make sure to *not* update the counter cache, or else - # it will be decremented twice. - # - # Hence this method. - def inverse_which_updates_counter_cache(reflection = reflection()) - counter_name = cached_counter_attribute_name(reflection) - inverse_which_updates_counter_named(counter_name, reflection) - end - alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - - def inverse_which_updates_counter_named(counter_name, reflection) - reflection.klass._reflections.values.find { |inverse_reflection| - inverse_reflection.belongs_to? && - inverse_reflection.counter_cache_column == counter_name - } - end - - def inverse_updates_counter_in_memory?(reflection) - inverse = inverse_which_updates_counter_cache(reflection) - inverse && inverse == reflection.inverse_of - end - - def counter_must_be_updated_by_has_many?(reflection) - !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) - end - def delete_count(method, scope) if method == :delete_all scope.delete_all @@ -175,7 +124,7 @@ module ActiveRecord def delete_records(records, method) if method == :destroy records.each(&:destroy!) - update_counter(-records.length) unless inverse_updates_counter_cache? + update_counter(-records.length) unless reflection.inverse_updates_counter_cache? else scope = self.scope.where(reflection.klass.primary_key => records) update_counter(-delete_count(method, scope)) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 1aa6a2ca74..deb0f8c9f5 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -110,7 +110,7 @@ module ActiveRecord def update_through_counter?(method) case method when :destroy - !inverse_updates_counter_cache?(through_reflection) + !through_reflection.inverse_updates_counter_cache? when :nullify false else diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1829453d73..0fe9b2e81b 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,5 +1,5 @@ module ActiveRecord - # = Active Record Belongs To Has One Association + # = Active Record Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: include ForeignAssociation @@ -11,7 +11,7 @@ module ActiveRecord when :restrict_with_error if load_target - record = klass.human_attribute_name(reflection.name).downcase + record = owner.class.human_attribute_name(reflection.name).downcase message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil if message ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 0862306749..99b500526d 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/filters' require 'mutex_m' -require 'thread_safe' +require 'concurrent' module ActiveRecord # = Active Record Attribute Methods @@ -37,7 +37,7 @@ module ActiveRecord class AttributeMethodCache def initialize @module = Module.new - @method_cache = ThreadSafe::Cache.new + @method_cache = Concurrent::Map.new end def [](name) @@ -96,7 +96,7 @@ module ActiveRecord end end - # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # Raises an <tt>ActiveRecord::DangerousAttributeError</tt> exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 553122a5fc..10498f4322 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -19,7 +19,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) !value.blank? end elsif value.respond_to?(:zero?) diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 07d5e7d38e..bbf2a51a0e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -45,7 +45,7 @@ module ActiveRecord write_attribute_with_type_cast(attr_name, value, true) end - def raw_write_attribute(attr_name, value) + def raw_write_attribute(attr_name, value) # :nodoc: write_attribute_with_type_cast(attr_name, value, false) end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index e85777c335..0c730d313f 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,3 +1,5 @@ +require 'active_record/attribute' + module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 282af220fb..b579bc1e93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,5 +1,5 @@ require 'thread' -require 'thread_safe' +require 'concurrent' require 'monitor' module ActiveRecord @@ -337,7 +337,7 @@ module ActiveRecord # that case +conn.owner+ attr should be consulted. # Access and modification of +@thread_cached_conns+ does not require # synchronization. - @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size) + @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size) @connections = [] @automatic_reconnect = true @@ -364,7 +364,7 @@ module ActiveRecord # Is there an open connection that is being used for the current thread? # - # This method only works for connections that have been abtained through + # This method only works for connections that have been obtained through # #connection or #with_connection methods, connections obtained through # #checkout will not be detected by #active_connection? def active_connection? @@ -427,7 +427,7 @@ module ActiveRecord # The pool first tries to gain ownership of all connections, if unable to # do so within a timeout interval (default duration is # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully - # disconneted wihout any regard for other connection owning threads. + # disconnected without any regard for other connection owning threads. def disconnect! disconnect(false) end @@ -587,7 +587,7 @@ module ActiveRecord end #-- - # From the discussion on Github: + # From the discussion on GitHub: # https://github.com/rails/rails/pull/14938#commitcomment-6601951 # This hook-in method allows for easier monkey-patching fixes needed by # JRuby users that use Fibers. @@ -824,11 +824,11 @@ module ActiveRecord # These caches are keyed by klass.name, NOT klass. Keying them by klass # alone would lead to memory leaks in development mode as all previous # instances of the class would stay in memory. - @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new(:initial_capacity => 2) + @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| + h[k] = Concurrent::Map.new(:initial_capacity => 2) end - @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new + @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| + h[k] = Concurrent::Map.new end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 1e13b24867..107806cd93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -137,7 +137,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -190,7 +190,7 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/current/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 18d943f452..0ba4d94e3c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,8 +14,10 @@ module ActiveRecord send m, o end - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options private @@ -38,17 +40,32 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} " - create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + if supports_indexes_in_create? + statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) + end + + if supports_foreign_keys? + statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) + end + + create_sql << "(#{statements.join(', ')}) " if statements.present? create_sql << "#{o.options}" create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end - def visit_AddForeignKey(o) + def visit_PrimaryKeyDefinition(o) + "PRIMARY KEY (#{o.name.join(', ')})" + end + + def visit_ForeignKeyDefinition(o) sql = <<-SQL.strip_heredoc - ADD CONSTRAINT #{quote_column_name(o.name)} + CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL @@ -57,6 +74,10 @@ module ActiveRecord sql end + def visit_AddForeignKey(o) + "ADD #{accept(o)}" + end + def visit_DropForeignKey(name) "DROP CONSTRAINT #{quote_column_name(name)}" end @@ -89,8 +110,9 @@ module ActiveRecord sql end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def foreign_key_in_create(from_table, to_table, options) + options = foreign_key_options(from_table, to_table, options) + accept ForeignKeyDefinition.new(from_table, to_table, options) end def action_sql(action, dependency) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 3115e03ea2..10329de5f4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -23,6 +23,9 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end + class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: + end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: def name options[:name] @@ -207,6 +210,7 @@ module ActiveRecord @columns_hash = {} @indexes = {} @foreign_keys = {} + @primary_keys = nil @native = types @temporary = temporary @options = options @@ -214,6 +218,11 @@ module ActiveRecord @name = name end + def primary_keys(name = nil) # :nodoc: + @primary_keys = PrimaryKeyDefinition.new(name) if name + @primary_keys + end + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end @@ -236,8 +245,8 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and - # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns. + # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. # * <tt>:null</tt> - diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7974ff3710..285b4065b1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -23,6 +23,20 @@ module ActiveRecord table_name[0...table_alias_length].tr('.', '_') end + # Returns the relation names useable to back Active Record models. + # For most adapters this means all #tables and #views. + def data_sources + tables | views + end + + # Checks to see if the data source +name+ exists on the database. + # + # data_source_exists?(:ebooks) + # + def data_source_exists?(name) + data_sources.include?(name.to_s) + end + # Returns an array of table names defined in the database. def tables(name = nil) raise NotImplementedError, "#tables is not implemented" @@ -36,6 +50,19 @@ module ActiveRecord tables.include?(table_name.to_s) end + # Returns an array of view names defined in the database. + def views + raise NotImplementedError, "#views is not implemented" + end + + # Checks to see if the view +view_name+ exists on the database. + # + # view_exists?(:ebooks) + # + def view_exists?(view_name) + views.include?(view_name.to_s) + end + # Returns an array of indexes for the given table. # def indexes(table_name, name = nil) end @@ -93,6 +120,12 @@ module ActiveRecord (!options.key?(:null) || c.null == options[:null]) } end + # Returns just a table's primary key + def primary_key(table_name) + pks = primary_keys(table_name) + pks.first if pks.one? + end + # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # @@ -158,7 +191,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # id int auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -170,7 +203,7 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # guid int auto_increment PRIMARY KEY, # name varchar(80) # ) # @@ -220,7 +253,11 @@ module ActiveRecord Base.get_primary_key table_name.to_s.singularize end - td.primary_key pk, options.fetch(:id, :primary_key), options + if pk.is_a?(Array) + td.primary_keys pk + else + td.primary_key pk, options.fetch(:id, :primary_key), options + end end yield td if block_given? @@ -237,10 +274,6 @@ module ActiveRecord end end - td.foreign_keys.each_pair do |other_table_name, foreign_key_options| - add_foreign_key(table_name, other_table_name, foreign_key_options) - end - result end @@ -320,7 +353,7 @@ module ActiveRecord # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as # - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ... # # Defaults to false. # @@ -540,6 +573,8 @@ module ActiveRecord # # CREATE INDEX by_name ON accounts(name(10)) # + # ====== Creating an index with specific key lengths for multiple keys + # # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # # generates: @@ -769,15 +804,7 @@ module ActiveRecord def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? - options[:column] ||= foreign_key_column_for(to_table) - - options = { - column: options[:column], - primary_key: options[:primary_key], - name: foreign_key_name(from_table, options), - on_delete: options[:on_delete], - on_update: options[:on_update] - } + options = foreign_key_options(from_table, to_table, options) at = create_alter_table from_table at.add_foreign_key to_table, options @@ -845,6 +872,13 @@ module ActiveRecord "#{name.singularize}_id" end + def foreign_key_options(from_table, to_table, options) # :nodoc: + options = options.dup + options[:column] ||= foreign_key_column_for(to_table) + options[:name] ||= foreign_key_name(from_table, options) + options + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -859,7 +893,7 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end - def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) @@ -986,6 +1020,10 @@ module ActiveRecord [index_name, index_type, index_columns, index_options, algorithm, using] end + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + protected def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -1012,10 +1050,6 @@ module ActiveRecord column_names.map {|name| quote_column_name(name) + option_strings[name]} end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 7b47539596..79a24d1996 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -14,10 +14,26 @@ module ActiveRecord def json(*args, **options) args.each { |name| column(name, :json, options) } end + + def unsigned_integer(*args, **options) + args.each { |name| column(name, :unsigned_integer, options) } + end + + def unsigned_bigint(*args, **options) + args.each { |name| column(name, :unsigned_bigint, options) } + end + + def unsigned_float(*args, **options) + args.each { |name| column(name, :unsigned_float, options) } + end + + def unsigned_decimal(*args, **options) + args.each { |name| column(name, :unsigned_decimal, options) } + end end class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset + attr_accessor :charset, :unsigned end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -29,7 +45,11 @@ module ActiveRecord when :primary_key column.type = :integer column.auto_increment = true + when /\Aunsigned_(?<type>.+)\z/ + column.type = $~[:type].to_sym + column.unsigned = true end + column.unsigned ||= options[:unsigned] column.charset = options[:charset] column end @@ -52,17 +72,9 @@ module ActiveRecord "DROP FOREIGN KEY #{name}" end - def visit_TableDefinition(o) - name = o.name - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " - - statements = o.columns.map { |c| accept c } - statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) - - create_sql << "(#{statements.join(', ')}) " if statements.present? - create_sql << "#{o.options}" - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as - create_sql + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) + super end def visit_AddColumnDefinition(o) @@ -117,6 +129,7 @@ module ActiveRecord spec = {} if column.auto_increment? spec[:id] = ':bigint' if column.bigint? + spec[:unsigned] = 'true' if column.unsigned? return if spec.empty? else spec[:id] = column.type.inspect @@ -125,6 +138,16 @@ module ActiveRecord spec end + def prepare_column_options(column) + spec = super + spec[:unsigned] = 'true' if column.unsigned? + spec + end + + def migration_keys + super + [:unsigned] + end + private def schema_limit(column) @@ -171,6 +194,10 @@ module ActiveRecord sql_type =~ /blob/i || type == :text end + def unsigned? + /unsigned/ === sql_type + end + def case_sensitive? collation && !collation.match(/_ci$/) end @@ -246,7 +273,7 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - primary_key: "int(11) auto_increment PRIMARY KEY", + primary_key: "int auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, text: { name: "text" }, integer: { name: "int", limit: 4 }, @@ -316,6 +343,10 @@ module ActiveRecord version >= '5.0.0' end + def supports_explain? + true + end + def supports_indexes_in_create? true end @@ -417,6 +448,80 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + ExplainPrettyPrinter.new.pp(result, elapsed) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + def clear_cache! super reload_type_map @@ -520,33 +625,42 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil, like = nil) #:nodoc: - sql = "SHOW TABLES " - sql << "IN #{quote_table_name(database)} " if database - sql << "LIKE #{quote(like)}" if like - - execute_and_free(sql, 'SCHEMA') do |result| - result.collect(&:first) - end + def tables(name = nil) # :nodoc: + select_values("SHOW FULL TABLES", 'SCHEMA') end + alias data_sources tables def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - def table_exists?(name) - return false unless name.present? - return true if tables(nil, nil, name).any? + def table_exists?(table_name) + return false unless table_name.present? - name = name.to_s - schema, table = name.split('.', 2) + schema, name = table_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A table was provided without a schema - unless table # A table was provided without a schema - table = schema - schema = nil - end + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? + end + alias data_source_exists? table_exists? - tables(nil, schema, table).any? + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + schema, name = view_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A view was provided without a schema + + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? end # Returns an array of indexes for the given table. @@ -685,7 +799,7 @@ module ActiveRecord AND fk.table_name = '#{table_name}' SQL - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info = create_table_info(table_name) fk_info.map do |row| options = { @@ -702,7 +816,7 @@ module ActiveRecord end def table_options(table_name) - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info = create_table_info(table_name) # strip create_definitions and partition_options raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip @@ -712,8 +826,8 @@ module ActiveRecord end # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s + def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + sql = case type.to_s when 'binary' binary_to_sql(limit) when 'integer' @@ -721,8 +835,11 @@ module ActiveRecord when 'text' text_to_sql(limit) else - super + super(type, limit, precision, scale) end + + sql << ' unsigned' if unsigned && type != :primary_key + sql end # SHOW VARIABLES LIKE 'name' @@ -735,21 +852,25 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) - execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| - create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ - keys = $1.split(",").map { |key| key.delete('`"') } - keys.length == 1 ? [keys.first, nil] : nil - else - nil - end + if pk = primary_key(table) + [ pk, nil ] end end - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first + def primary_keys(table_name) # :nodoc: + raise ArgumentError unless table_name.present? + + schema, name = table_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A table was provided without a schema + + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT column_name + FROM information_schema.key_column_usage + WHERE constraint_name = 'PRIMARY' + AND table_schema = #{quote(schema)} + AND table_name = #{quote(name)} + ORDER BY ordinal_position + SQL end def case_sensitive_modifier(node, table_attribute) @@ -807,7 +928,6 @@ module ActiveRecord register_integer_type m, %r(^tinyint)i, limit: 1 m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans - m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' @@ -816,6 +936,12 @@ module ActiveRecord .split(',').map{|enum| enum.strip.length - 2}.max MysqlString.new(limit: limit) end + + m.register_type(%r(^set)i) do |sql_type| + limit = sql_type[/^set\((.+)\)/i, 1] + .split(',').map{|set| set.strip.length - 1}.sum - 1 + MysqlString.new(limit: limit) + end end def register_integer_type(mapping, key, options) # :nodoc: @@ -980,14 +1106,14 @@ module ActiveRecord defaults = [':default', :default].to_set # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict]) variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" @@ -1018,6 +1144,11 @@ module ActiveRecord end end + def create_table_info(table_name) # :nodoc: + @create_table_info_cache = {} + @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + end + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: TableDefinition.new(native_database_types, name, temporary, options, as) end @@ -1027,7 +1158,7 @@ module ActiveRecord when 0..0xfff; "varbinary(#{limit})" when nil; "blob" when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") + else raise(ActiveRecordError, "No binary type has byte length #{limit}") end end @@ -1036,8 +1167,9 @@ module ActiveRecord when 1; 'tinyint' when 2; 'smallint' when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when nil, 4; 'int' when 5..8; 'bigint' + when 11; 'int(11)' # backward compatibility with Rails 2.0 else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end @@ -1048,7 +1180,7 @@ module ActiveRecord when nil, 0x100..0xffff; 'text' when 0x10000..0xffffff; 'mediumtext' when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") + else raise(ActiveRecordError, "No text type has byte length #{limit}") end end @@ -1084,6 +1216,8 @@ module ActiveRecord ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 4b95b0681d..5e31efec4a 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,20 +5,13 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 734b384e80..4461722bb4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -37,10 +37,6 @@ module ActiveRecord configure_connection end - def supports_explain? - true - end - def supports_json? version >= '5.7.8' end @@ -99,80 +95,6 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds.dup)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN', binds) - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed - end - end - # FIXME: re-enable the following once a "better" query_cache solution is in core # # The overrides below perform much better than the originals in AbstractAdapter diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 0738c59ddf..b3894481cc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -58,9 +58,9 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -80,8 +80,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @client_encoding = nil connect end @@ -162,6 +161,14 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ + def select_all(arel, name = nil, binds = []) + if ExplainRegistry.collect? && prepared_statements + unprepared_statement { super } + else + super + end + end + def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true rows = exec_query(sql, name, binds).rows diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 2c04c46131..424769f765 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -4,18 +4,14 @@ module ActiveRecord module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: def cast_value(value) - if value.is_a?(::String) - case value - when 'infinity' then ::Float::INFINITY - when '-infinity' then -::Float::INFINITY - when / BC$/ - astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) - super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) - else - super - end + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) else - value + super end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 69aa02ccf4..aaf5b2898b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -73,6 +73,16 @@ module ActiveRecord select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end + def data_sources # :nodoc + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + # Returns true if table exists. # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) @@ -89,6 +99,31 @@ module ActiveRecord AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end + alias data_source_exists? table_exists? + + def views # :nodoc: + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + + def view_exists?(view_name) # :nodoc: + name = Utils.extract_schema_qualified_name(view_name.to_s) + return false unless name.identifier + + select_values(<<-SQL, 'SCHEMA').any? + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + SQL + end def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" @@ -353,17 +388,19 @@ module ActiveRecord nil end - # Returns just a table's primary key - def primary_key(table) - pks = query(<<-end_sql, 'SCHEMA') - SELECT attr.attname - FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) - WHERE cons.contype = 'p' - AND cons.conrelid = '#{quote_table_name(table)}'::regclass - end_sql - return nil unless pks.count == 1 - pks[0][0] + def primary_keys(table_name) # :nodoc: + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + WITH pk_constraint AS ( + SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint + WHERE contype = 'p' + AND conrelid = '#{quote_table_name(table_name)}'::regclass + ), cons AS ( + SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint + ) + SELECT attr.attname FROM pg_attribute attr + INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum + ORDER BY cons.rownum + SQL end # Renames a table. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 27291bd2ea..25dfda9ef8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -211,7 +211,8 @@ module ActiveRecord class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) - super + super(max) + @connection = connection @counter = 0 end @@ -760,7 +761,7 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) # :nodoc: - sql[/into\s+([^\(]*).*values\s*\(/im] + sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 981d5d7a3c..eee142378c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -10,7 +10,7 @@ module ActiveRecord @columns = {} @columns_hash = {} @primary_keys = {} - @tables = {} + @data_sources = {} end def initialize_dup(other) @@ -18,33 +18,38 @@ module ActiveRecord @columns = @columns.dup @columns_hash = @columns_hash.dup @primary_keys = @primary_keys.dup - @tables = @tables.dup + @data_sources = @data_sources.dup end def primary_keys(table_name) - @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. - def table_exists?(name) - prepare_tables if @tables.empty? - return @tables[name] if @tables.key? name + def data_source_exists?(name) + prepare_data_sources if @data_sources.empty? + return @data_sources[name] if @data_sources.key? name - @tables[name] = connection.table_exists?(name) + @data_sources[name] = connection.data_source_exists?(name) end + alias table_exists? data_source_exists? + deprecate :table_exists? => "use #data_source_exists? instead" + # Add internal cache for table with +table_name+. def add(table_name) - if table_exists?(table_name) + if data_source_exists?(table_name) primary_keys(table_name) columns(table_name) columns_hash(table_name) end end - def tables(name) - @tables[name] + def data_sources(name) + @data_sources[name] end + alias tables data_sources + deprecate :tables => "use #data_sources instead" # Get the columns for a table def columns(table_name) @@ -64,36 +69,38 @@ module ActiveRecord @columns.clear @columns_hash.clear @primary_keys.clear - @tables.clear + @data_sources.clear @version = nil end def size - [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ + [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+ end - # Clear out internal caches for table with +table_name+. - def clear_table_cache!(table_name) - @columns.delete table_name - @columns_hash.delete table_name - @primary_keys.delete table_name - @tables.delete table_name + # Clear out internal caches for the data source +name+. + def clear_data_source_cache!(name) + @columns.delete name + @columns_hash.delete name + @primary_keys.delete name + @data_sources.delete name end + alias clear_table_cache! clear_data_source_cache! + deprecate :clear_table_cache! => "use #clear_data_source_cache! instead" def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version, @columns, @columns_hash, @primary_keys, @tables] + [@version, @columns, @columns_hash, @primary_keys, @data_sources] end def marshal_load(array) - @version, @columns, @columns_hash, @primary_keys, @tables = array + @version, @columns, @columns_hash, @primary_keys, @data_sources = array end private - def prepare_tables - connection.tables.each { |table| @tables[table] = true } + def prepare_data_sources + connection.data_sources.each { |source| @data_sources[source] = true } end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 24fc67938d..f6a6e3eb4d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -81,8 +81,7 @@ module ActiveRecord super(connection, logger) @active = nil - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config @visitor = Arel::Visitors::SQLite.new self @@ -320,10 +319,25 @@ module ActiveRecord row['name'] end end + alias data_sources tables def table_exists?(table_name) table_name && tables(nil, table_name).any? end + alias data_source_exists? table_exists? + + def views # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(view_name)}" + + select_values(sql, 'SCHEMA').any? + end # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: @@ -369,10 +383,9 @@ module ActiveRecord end end - def primary_key(table_name) #:nodoc: + def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f['pk'] > 0 } - return nil unless pks.count == 1 - pks[0]['name'] + pks.sort_by { |f| f['pk'] }.map { |f| f['name'] } end def remove_index(table_name, options = {}) #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 82e9ef3d3d..57463dd749 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -3,10 +3,9 @@ module ActiveRecord class StatementPool include Enumerable - def initialize(connection, max = 1000) + def initialize(max = 1000) @cache = Hash.new { |h,pid| h[pid] = {} } - @connection = connection - @max = max + @max = max end def each(&block) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 4902fcb1a2..a79bf0366b 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -204,30 +204,22 @@ module ActiveRecord def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'class', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name, type: 'class') elsif !klass_method && dangerous_attribute_method?(method_name) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'Active Record' - } + raise_conflict_error(enum_name, method_name) elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) - raise ArgumentError, ENUM_CONFLICT_MESSAGE % { - enum: enum_name, - klass: self.name, - type: 'instance', - method: method_name, - source: 'another enum' - } + raise_conflict_error(enum_name, method_name, source: 'another enum') end end + + def raise_conflict_error(enum_name, method_name, type: 'instance', source: 'Active Record') + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: type, + method: method_name, + source: source + } + end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 15b2f65dcb..370c1562c9 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -84,7 +84,7 @@ module ActiveRecord # Values longer than 20 characters will be truncated. The value # is truncated word by word. # - # user = User.find_by(name: 'David HeinemeierHansson') + # user = User.find_by(name: 'David Heinemeier Hansson') # user.id # => 125 # user_path(user) # => "/users/125-david" # diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index a09437b4b0..2336d23a1c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -22,7 +22,7 @@ module ActiveRecord # p1.save # # p2.first_name = "should fail" - # p2.save # Raises a ActiveRecord::StaleObjectError + # p2.save # Raises an ActiveRecord::StaleObjectError # # Optimistic locking will also check for stale data when objects are destroyed. Example: # @@ -32,7 +32,7 @@ module ActiveRecord # p1.first_name = "Michael" # p1.save # - # p2.destroy # Raises a ActiveRecord::StaleObjectError + # p2.destroy # Raises an ActiveRecord::StaleObjectError # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 3d95c54ef3..8ecdf76b72 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 4d597a0ab1..b63caa4473 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -47,20 +47,40 @@ module ActiveRecord binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end - name = color(name, nil, true) + name = colorize_payload_name(name, payload[:name]) sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end + private + + def colorize_payload_name(name, payload_name) + if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists + color(name, MAGENTA, true) + else + color(name, CYAN, true) + end + end + def sql_color(sql) case sql - when /\s*\Ainsert/i then GREEN - when /\s*\Aselect/i then BLUE - when /\s*\Aupdate/i then YELLOW - when /\s*\Adelete/i then RED - when /transaction\s*\Z/i then CYAN - else MAGENTA + when /\A\s*rollback/mi + RED + when /\s*.*?select .*for update/mi, /\A\s*lock/mi + WHITE + when /\A\s*select/i + BLUE + when /\A\s*insert/i + GREEN + when /\A\s*update/i + YELLOW + when /\A\s*delete/i + RED + when /transaction\s*\Z/i + CYAN + else + MAGENTA end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 3b90ab1e31..5bdb7213cd 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -941,10 +941,6 @@ module ActiveRecord migrations(migrations_paths).any? end - def last_version - last_migration.version - end - def last_migration #:nodoc: migrations(migrations_paths).last || NullMigration.new end @@ -955,10 +951,6 @@ module ActiveRecord Array(@migrations_paths) end - def migrations_path - migrations_paths.first - end - def migrations(paths) paths = Array(paths) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 2b0c755ef4..c461c04a26 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -213,7 +213,7 @@ module ActiveRecord # Indicates whether the table associated with this class exists def table_exists? - connection.schema_cache.table_exists?(table_name) + connection.schema_cache.data_source_exists?(table_name) end def attributes_builder # :nodoc: @@ -290,7 +290,7 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) + connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 74894d0c37..0b500346bc 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - module ActiveRecord module NullRelation # :nodoc: def exec_queries diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 63ea305eae..d940ac244a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -79,7 +79,7 @@ db_namespace = namespace :db do task :up => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required' unless version - ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -87,7 +87,7 @@ db_namespace = namespace :db do task :down => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required - To go down one migration, run db:rollback' unless version - ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -99,7 +99,7 @@ db_namespace = namespace :db do db_list = ActiveRecord::SchemaMigration.normalized_versions file_list = - ActiveRecord::Migrator.migrations_paths.flat_map do |path| + ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do version = ActiveRecord::SchemaMigration.normalize_migration_number($1) @@ -125,14 +125,14 @@ db_namespace = namespace :db do desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' task :rollback => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task :forward => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end @@ -160,7 +160,7 @@ db_namespace = namespace :db do # desc "Raises an error if there are pending migrations" task :abort_if_pending_migrations => [:environment, :load_config] do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -255,7 +255,7 @@ db_namespace = namespace :db do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") con.schema_cache.clear! - con.tables.each { |table| con.schema_cache.add(table) } + con.data_sources.each { |table| con.schema_cache.add(table) } open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } end @@ -353,7 +353,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end - # desc 'Check for pending migrations and load the test schema' + # desc 'Load the test schema' task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke @@ -384,7 +384,7 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, + ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties, :on_skip => on_skip, :on_copy => on_copy) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f8913eba06..4e0fc407bc 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -66,7 +66,6 @@ module ActiveRecord # # Account.reflections # => {"balance" => AggregateReflection} # - # @api public def reflections @__reflections ||= begin ref = {} @@ -96,10 +95,10 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # - # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values - macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections + association_reflections.select! { |reflection| reflection.macro == macro } if macro + association_reflections end # Returns the AssociationReflection object for the +association+ (use the symbol). @@ -107,24 +106,20 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # - # @api public def reflect_on_association(association) reflections[association.to_s] end - # @api private def _reflect_on_association(association) #:nodoc: _reflections[association.to_s] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. - # - # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end - def clear_reflections_cache #:nodoc: + def clear_reflections_cache # :nodoc: @__reflections = nil end end @@ -168,6 +163,54 @@ module ActiveRecord scope_chain.flatten end + def counter_cache_column + if belongs_to? + if options[:counter_cache] == true + "#{active_record.name.demodulize.underscore.pluralize}_count" + elsif options[:counter_cache] + options[:counter_cache].to_s + end + else + options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count" + end + end + + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_which_updates_counter_cache + return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache) + @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse| + inverse.counter_cache_column == counter_cache_column + end + end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache + + def inverse_updates_counter_in_memory? + inverse_of && inverse_which_updates_counter_cache == inverse_of + end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. + def has_cached_counter? + options[:counter_cache] || + inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] && + !!active_record.columns_hash[counter_cache_column] + end + + def counter_must_be_updated_by_has_many? + !inverse_updates_counter_in_memory? && has_cached_counter? + end + def alias_candidate(name) "#{plural_name}_#{name}" end @@ -330,14 +373,6 @@ module ActiveRecord @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end - def counter_cache_column - if options[:counter_cache] == true - "#{active_record.name.demodulize.underscore.pluralize}_count" - elsif options[:counter_cache] - options[:counter_cache].to_s - end - end - def check_validity! check_validity_of_inverse! end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index bf08cdbbf3..e596df8742 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "arel/collectors/bind" module ActiveRecord diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0f6015fa93..c562bd9ce4 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -70,9 +70,9 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum(:age) # => 4562 - def sum(*args) - return super if block_given? - calculate(:sum, *args) + def sum(column_name = nil, &block) + return super(&block) if block_given? + calculate(:sum, column_name) end # This calculates aggregate values in the given column. Methods for count, sum, average, diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index e25b889851..038280509d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -250,7 +250,7 @@ module ActiveRecord def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| - klass.attribute_alias?(field) ? klass.attribute_alias(field) : field + klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field end self.select_values += fields self diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index ba75ffa5a1..32f4229977 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -15,7 +15,7 @@ module ActiveRecord # them into a valid SQL fragment for a WHERE clause. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = self.table_name) + def sanitize_sql_for_conditions(condition) return nil if condition.blank? case condition diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 0a5546a760..c1a42dc629 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -28,24 +28,6 @@ module ActiveRecord # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. class Schema < Migration - - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end - - def define(info, &block) # :nodoc: - instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) - end - end - # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -60,5 +42,23 @@ module ActiveRecord def self.define(info={}, &block) new.define(info, &block) end + + def define(info, &block) # :nodoc: + instance_eval(&block) + + if info[:version].present? + initialize_schema_migrations_table + connection.assume_migrated_upto_version(info[:version], migrations_paths) + end + end + + private + # Returns the migrations paths. + # + # ActiveRecord::Schema.new.migrations_paths + # # => ["db/migrate"] # Rails migration path by default. + def migrations_paths # :nodoc: + ActiveRecord::Migrator.migrations_paths + end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index c5910fa1ad..2362dae9fc 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -89,7 +89,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.tables.sort + sorted_tables = @connection.data_sources.sort - @connection.views sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) @@ -112,20 +112,27 @@ HEADER tbl = StringIO.new # first dump primary key column - pk = @connection.primary_key(table) + if @connection.respond_to?(:primary_keys) + pk = @connection.primary_keys(table) + pk = pk.first unless pk.size > 1 + else + pk = @connection.primary_key(table) + end tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - pkcol = columns.detect { |c| c.name == pk } - if pkcol - if pk != 'id' - tbl.print %Q(, primary_key: "#{pk}") - end + + case pk + when String + tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id' + pkcol = columns.detect { |c| c.name == pk } pkcolspec = @connection.column_spec_for_primary_key(pkcol) if pkcolspec pkcolspec.each do |key, value| tbl.print ", #{key}: #{value}" end end + when Array + tbl.print ", primary_key: #{pk.inspect}" else tbl.print ", id: false" end @@ -247,7 +254,7 @@ HEADER end def ignored?(table_name) - ['schema_migrations', ignore_tables].flatten.any? do |ignored| + [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored| ignored === remove_prefix_and_suffix(table_name) end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index cb47bf23f7..b384529e75 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -2,7 +2,11 @@ require 'active_record/scoping/default' require 'active_record/scoping/named' module ActiveRecord - class SchemaMigration < ActiveRecord::Base + # This class is used to create a table that keeps track of which migrations + # have been applied to a given database. When a migration is run, its schema + # number is inserted in to the `SchemaMigration.table_name` so it doesn't need + # to be executed the next time. + class SchemaMigration < ActiveRecord::Base # :nodoc: class << self def primary_key nil diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 0b5dc6ed33..f1141a8613 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -221,12 +221,6 @@ module ActiveRecord end end - def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - if File.exist?(file || schema_file(format)) - load_schema_current(format, file, environment) - end - end - def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| load_schema configuration, format, file diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 673386f0d9..8929aa85c8 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -23,7 +23,7 @@ module ActiveRecord end rescue error_class => error if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR - $stdout.print error.error + $stdout.print error.message establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options if configuration['username'] != 'root' @@ -56,22 +56,21 @@ module ActiveRecord end def structure_dump(filename) - args = prepare_command_options('mysqldump') + args = prepare_command_options args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["--routines"]) args.concat(["#{configuration['database']}"]) - unless Kernel.system(*args) - $stderr.puts "Could not dump the database structure. "\ - "Make sure `mysqldump` is in your PATH and check the command output for warnings." - end + + run_cmd('mysqldump', args, 'dumping') end def structure_load(filename) - args = prepare_command_options('mysql') + args = prepare_command_options args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) - Kernel.system(*args) + + run_cmd('mysql', args, 'loading') end private @@ -130,7 +129,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; $stdin.gets.strip end - def prepare_command_options(command) + def prepare_command_options args = { 'host' => '--host', 'port' => '--port', @@ -145,7 +144,17 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; 'sslkey' => '--ssl-key' }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact - [command, *args] + args + end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute: `#{cmd}`\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 20e4235788..e759475cfb 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -15,14 +15,21 @@ module ActiveRecord # # == Time Zone aware attributes # - # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. + # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns + # time-zone aware. By default, these values are stored in the database as UTC + # and converted back to the current Time.zone when pulled from the database. # - # config.active_record.time_zone_aware_attributes = true + # This feature can be turned off completely by setting: # - # This feature can easily be turned off by assigning value <tt>false</tt> . + # config.active_record.time_zone_aware_attributes = false # - # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone - # when reading certain attributes then you can do following: + # You can also specify that only <tt>datetime</tt> columns should be time-zone + # aware (while <tt>time</tt> should not) by setting: + # + # ActiveRecord::Base.time_zone_aware_types = [:datetime] + # + # Finally, you can indicate specific attributes of a model for which time zone + # conversion should not applied, for instance by setting: # # class Topic < ActiveRecord::Base # self.skip_time_zone_conversion_for_attributes = [:written_on] diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 887d7a5903..1a2988ea77 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,6 +11,7 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, + terminator: deprecated_false_terminator, scope: [:kind, :name] end @@ -167,7 +168,7 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # # === Callbacks diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 53f3b53bec..74dfe88349 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,27 +1,18 @@ -require 'active_record/type/helpers' -require 'active_record/type/value' +require 'active_model/type' + +require 'active_record/type/internal/abstract_json' +require 'active_record/type/internal/timezone' -require 'active_record/type/big_integer' -require 'active_record/type/binary' -require 'active_record/type/boolean' require 'active_record/type/date' require 'active_record/type/date_time' -require 'active_record/type/decimal' -require 'active_record/type/decimal_without_scale' -require 'active_record/type/float' -require 'active_record/type/integer' -require 'active_record/type/serialized' -require 'active_record/type/string' -require 'active_record/type/text' require 'active_record/type/time' -require 'active_record/type/unsigned_integer' +require 'active_record/type/serialized' require 'active_record/type/adapter_specific_registry' + require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' -require 'active_record/type/internal/abstract_json' - module ActiveRecord module Type @registry = AdapterSpecificRegistry.new @@ -53,6 +44,19 @@ module ActiveRecord end end + Helpers = ActiveModel::Type::Helpers + BigInteger = ActiveModel::Type::BigInteger + Binary = ActiveModel::Type::Binary + Boolean = ActiveModel::Type::Boolean + Decimal = ActiveModel::Type::Decimal + DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale + Float = ActiveModel::Type::Float + Integer = ActiveModel::Type::Integer + String = ActiveModel::Type::String + Text = ActiveModel::Type::Text + UnsignedInteger = ActiveModel::Type::UnsignedInteger + Value = ActiveModel::Type::Value + register(:big_integer, Type::BigInteger, override: false) register(:binary, Type::Binary, override: false) register(:boolean, Type::Boolean, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index 5f71b3cb94..d440eac619 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -1,35 +1,24 @@ +require 'active_model/type/registry' + module ActiveRecord # :stopdoc: module Type - class AdapterSpecificRegistry - def initialize - @registrations = [] - end - - def register(type_name, klass = nil, **options, &block) - block ||= proc { |_, *args| klass.new(*args) } - registrations << Registration.new(type_name, block, **options) + class AdapterSpecificRegistry < ActiveModel::Type::Registry + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) end - def lookup(symbol, *args) - registration = registrations - .select { |r| r.matches?(symbol, *args) } - .max + private - if registration - registration.call(self, symbol, *args) - else - raise ArgumentError, "Unknown type #{symbol.inspect}" - end + def registration_klass + Registration end - def add_modifier(options, klass, **args) - registrations << DecorationRegistration.new(options, klass, **args) + def find_registration(symbol, *args) + registrations + .select { |registration| registration.matches?(symbol, *args) } + .max end - - protected - - attr_reader :registrations end class Registration @@ -137,6 +126,5 @@ module ActiveRecord class TypeConflictError < StandardError end - # :startdoc: end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb index 3ceab59ebb..ccafed054e 100644 --- a/activerecord/lib/active_record/type/date.rb +++ b/activerecord/lib/active_record/type/date.rb @@ -1,49 +1,7 @@ module ActiveRecord module Type - class Date < Value # :nodoc: - include Helpers::AcceptsMultiparameterTime.new - - def type - :date - end - - def type_cast_for_schema(value) - "'#{value.to_s(:db)}'" - end - - private - - def cast_value(value) - if value.is_a?(::String) - return if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def fast_string_to_date(string) - if string =~ ConnectionAdapters::Column::Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def new_date(year, mon, mday) - if year && year != 0 - ::Date.new(year, mon, mday) rescue nil - end - end - - def value_from_multiparameter_assignment(*) - time = super - time && time.to_date - end + class Date < ActiveModel::Type::Date + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index a5199959b9..1fb9380ecd 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -1,44 +1,7 @@ module ActiveRecord module Type - class DateTime < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 4 => 0, 5 => 0 } - ) - - def type - :datetime - end - - private - - def cast_value(string) - return string unless string.is_a?(::String) - return if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def fallback_string_to_time(string) - time_hash = ::Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - - def value_from_multiparameter_assignment(values_hash) - missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } - if missing_parameter - raise ArgumentError, missing_parameter - end - super - end + class DateTime < ActiveModel::Type::DateTime + include Internal::Timezone end end end diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb deleted file mode 100644 index 634d417d13..0000000000 --- a/activerecord/lib/active_record/type/helpers.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_record/type/helpers/accepts_multiparameter_time' -require 'active_record/type/helpers/numeric' -require 'active_record/type/helpers/mutable' -require 'active_record/type/helpers/time_value' diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 963a8245d0..097d1bd363 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -1,8 +1,8 @@ module ActiveRecord module Type module Internal # :nodoc: - class AbstractJson < Type::Value # :nodoc: - include Type::Helpers::Mutable + class AbstractJson < ActiveModel::Type::Value # :nodoc: + include ActiveModel::Type::Helpers::Mutable def type :json diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb new file mode 100644 index 0000000000..947e06158a --- /dev/null +++ b/activerecord/lib/active_record/type/internal/timezone.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + module Internal + module Timezone + def is_utc? + ActiveRecord::Base.default_timezone == :utc + end + + def default_timezone + ActiveRecord::Base.default_timezone + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ea3e0d6a45..203a395415 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,7 @@ module ActiveRecord module Type - class Serialized < DelegateClass(Type::Value) # :nodoc: - include Helpers::Mutable + class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb index 19a10021bc..70988d84ff 100644 --- a/activerecord/lib/active_record/type/time.rb +++ b/activerecord/lib/active_record/type/time.rb @@ -1,42 +1,8 @@ module ActiveRecord module Type - class Time < Value # :nodoc: - include Helpers::TimeValue - include Helpers::AcceptsMultiparameterTime.new( - defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } - ) - - def type - :time - end - - def user_input_in_time_zone(value) - return unless value.present? - - case value - when ::String - value = "2000-01-01 #{value}" - when ::Time - value = value.change(year: 2000, day: 1, month: 1) - end - - super(value) - end - - private - - def cast_value(value) - return value unless value.is_a?(::String) - return if value.empty? - - dummy_time_value = "2000-01-01 #{value}" - - fast_string_to_time(dummy_time_value) || begin - time_hash = ::Date._parse(dummy_time_value) - return if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end + class Time < ActiveModel::Type::Time + include Internal::Timezone end end end + diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 09f5ba6b74..81d7ed39bb 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,12 +1,12 @@ -require 'thread_safe' +require 'concurrent' module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} - @cache = ThreadSafe::Cache.new do |h, key| - h.fetch_or_store(key, ThreadSafe::Cache.new) + @cache = Concurrent::Map.new do |h, key| + h.fetch_or_store(key, Concurrent::Map.new) end end @@ -57,7 +57,7 @@ module ActiveRecord end def default_value - @default_value ||= Value.new + @default_value ||= ActiveModel::Type::Value.new end end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 3878270770..868d08ed44 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -20,7 +20,7 @@ module ActiveRecord private def column_for(attribute_name) - if connection.schema_cache.table_exists?(table_name) + if connection.schema_cache.data_source_exists?(table_name) connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 74daece4e8..4113ca4561 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -60,7 +60,7 @@ module ActiveRecord # Validations with no <tt>:on</tt> option will run no matter the context. Validations with # some <tt>:on</tt> option will only run in the specified context. def valid?(context = nil) - context ||= (new_record? ? :create : :update) + context ||= default_validation_context output = super(context) errors.empty? && output end @@ -69,6 +69,10 @@ module ActiveRecord protected + def default_validation_context + new_record? ? :create : :update + end + def raise_validation_error raise(RecordInvalid.new(self)) end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 49a68fb94c..43c817e057 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -7,7 +7,7 @@ module ActiveRecord module ConnectionAdapters class FakeAdapter < AbstractAdapter - attr_accessor :tables, :primary_keys + attr_accessor :data_sources, :primary_keys @columns = Hash.new { |h,k| h[k] = [] } class << self @@ -16,7 +16,7 @@ module ActiveRecord def initialize(connection, logger) super - @tables = [] + @data_sources = [] @primary_keys = {} @columns = self.class.columns end @@ -37,7 +37,7 @@ module ActiveRecord @columns[table_name] end - def table_exists?(*) + def data_source_exists?(*) true end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 1712ff0ac6..62579a4a7a 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -36,6 +36,21 @@ module ActiveRecord assert !@connection.table_exists?(nil) end + def test_data_sources + data_sources = @connection.data_sources + assert data_sources.include?("accounts") + assert data_sources.include?("authors") + assert data_sources.include?("tasks") + assert data_sources.include?("topics") + end + + def test_data_source_exists? + assert @connection.data_source_exists?("accounts") + assert @connection.data_source_exists?(:accounts) + assert_not @connection.data_source_exists?("nonexistingtable") + assert_not @connection.data_source_exists?(nil) + end + def test_indexes idx_name = "accounts_idx" @@ -63,7 +78,7 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_charset assert_not_nil @connection.charset assert_not_equal 'character_set_database', @connection.charset diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index f0fd95ac16..0b5c9e1798 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -100,17 +100,15 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase assert_equal "DROP TABLE `people`", drop_table(:people) end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) - end + def test_create_mysql_database_with_encoding + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + end - def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) - end + def test_recreate_mysql_database_with_encoding + create_database(:luca, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) end def test_add_column diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index ddbc007b87..8b62998964 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -183,7 +183,7 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase def with_example_table(&block) definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, + `id` int auto_increment PRIMARY KEY, `data` varchar(255) SQL super(@connection, 'ex', definition, &block) diff --git a/activerecord/test/cases/adapters/mysql/explain_test.rb b/activerecord/test/cases/adapters/mysql/explain_test.rb new file mode 100644 index 0000000000..c44c1e6648 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/explain_test.rb @@ -0,0 +1,21 @@ +require "cases/helper" +require 'models/developer' +require 'models/computer' + +class MysqlExplainTest < ActiveRecord::MysqlTestCase + fixtures :developers + + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %r(developers |.* const), explain + end + + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %r(developers |.* const), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain + assert_match %r(audit_logs |.* ALL), explain + end +end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index b804cb45b9..29573d8e0d 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" require 'support/ddl_helper' @@ -66,14 +65,6 @@ module ActiveRecord end end - def test_tables_quoting - @conn.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) - end - def test_pk_and_sequence_for with_example_table do pk, seq = @conn.pk_and_sequence_for('ex') @@ -83,7 +74,7 @@ module ActiveRecord end def test_pk_and_sequence_for_with_non_standard_primary_key - with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do + with_example_table '`code` INT auto_increment, PRIMARY KEY (`code`)' do pk, seq = @conn.pk_and_sequence_for('ex') assert_equal 'code', pk assert_equal @conn.default_sequence_name('ex', 'code'), seq @@ -91,7 +82,7 @@ module ActiveRecord end def test_pk_and_sequence_for_with_custom_index_type_pk - with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do + with_example_table '`id` INT auto_increment, PRIMARY KEY USING BTREE (`id`)' do pk, seq = @conn.pk_and_sequence_for('ex') assert_equal 'id', pk assert_equal @conn.default_sequence_name('ex', 'id'), seq @@ -99,7 +90,7 @@ module ActiveRecord end def test_composite_primary_key - with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do + with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do assert_nil @conn.primary_key('ex') end end @@ -141,7 +132,7 @@ module ActiveRecord def with_example_table(definition = nil, &block) definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, + `id` int auto_increment PRIMARY KEY, `number` integer, `data` varchar(255) SQL diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb index 6be36566de..0d1f968022 100644 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -3,7 +3,7 @@ require 'cases/helper' class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new nil, 10 + cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new(10) cache['foo'] = 'bar' assert_equal 'bar', cache['foo'] diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb index ed9398a918..84c5394c2e 100644 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -1,6 +1,8 @@ require "cases/helper" +require "support/schema_dumping_helper" class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase + include SchemaDumpingHelper self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base @@ -9,12 +11,15 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table("unsigned_types", force: true) do |t| - t.column :unsigned_integer, "int unsigned" + t.integer :unsigned_integer, unsigned: true + t.bigint :unsigned_bigint, unsigned: true + t.float :unsigned_float, unsigned: true + t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 end end teardown do - @connection.drop_table "unsigned_types" + @connection.drop_table "unsigned_types", if_exists: true end test "unsigned int max value is in range" do @@ -26,5 +31,35 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase assert_raise(RangeError) do UnsignedType.create(unsigned_integer: -10) end + assert_raise(RangeError) do + UnsignedType.create(unsigned_bigint: -10) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_float: -10.0) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_decimal: -10.0) + end + end + + test "schema definition can use unsigned as the type" do + @connection.change_table("unsigned_types") do |t| + t.unsigned_integer :unsigned_integer_t + t.unsigned_bigint :unsigned_bigint_t + t.unsigned_float :unsigned_float_t + t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 + end + + @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| + assert column.unsigned? + end + end + + test "schema dump includes unsigned option" do + schema = dump_table_schema "unsigned_types" + assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema + assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema + assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 6558d60aa1..31dc69a45b 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -100,17 +100,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase assert_equal "DROP TABLE `people`", drop_table(:people) end - if current_adapter?(:Mysql2Adapter) - def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) - end + def test_create_mysql_database_with_encoding + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + end - def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) - end + def test_recreate_mysql_database_with_encoding + create_database(:luca, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) end def test_add_column diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 880a2123d2..faf2acb9cb 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -36,14 +36,6 @@ module ActiveRecord assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") end - def test_tables_quoting - @connection.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) - end - def test_dump_indexes index_a_name = 'index_key_tests_on_snack' index_b_name = 'index_key_tests_on_pizza' diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index 9e06db2519..a6f6dd21bb 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,6 +1,8 @@ require "cases/helper" +require "support/schema_dumping_helper" class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base @@ -9,12 +11,15 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table("unsigned_types", force: true) do |t| - t.column :unsigned_integer, "int unsigned" + t.integer :unsigned_integer, unsigned: true + t.bigint :unsigned_bigint, unsigned: true + t.float :unsigned_float, unsigned: true + t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 end end teardown do - @connection.drop_table "unsigned_types" + @connection.drop_table "unsigned_types", if_exists: true end test "unsigned int max value is in range" do @@ -26,5 +31,35 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase assert_raise(RangeError) do UnsignedType.create(unsigned_integer: -10) end + assert_raise(RangeError) do + UnsignedType.create(unsigned_bigint: -10) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_float: -10.0) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_decimal: -10.0) + end + end + + test "schema definition can use unsigned as the type" do + @connection.change_table("unsigned_types") do |t| + t.unsigned_integer :unsigned_integer_t + t.unsigned_bigint :unsigned_bigint_t + t.unsigned_float :unsigned_float_t + t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 + end + + @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| + assert column.unsigned? + end + end + + test "schema dump includes unsigned option" do + schema = dump_table_schema "unsigned_types" + assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema + assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema + assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index f242f32496..b3b121b4fb 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/schema_dumping_helper' diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 6e6850c4a9..e361521155 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -123,6 +123,20 @@ module ActiveRecord assert_equal expect.to_i, result.rows.first.first end + def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], 'id') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect.to_i, result.rows.first.first + end + + def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], 'id') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect.to_i, result.rows.first.first + end + def test_sql_for_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index bee612d8d3..f89a394f96 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -5,9 +5,11 @@ require 'support/schema_dumping_helper' module PGSchemaHelper def with_schema_search_path(schema_search_path) @connection.schema_search_path = schema_search_path + @connection.schema_cache.clear! yield if block_given? ensure @connection.schema_search_path = "'$user', public" + @connection.schema_cache.clear! end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index ef324183a7..559b951109 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -6,7 +6,7 @@ module ActiveRecord::ConnectionAdapters if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = StatementPool.new nil, 10 + cache = StatementPool.new(10) cache['foo'] = 'bar' assert_equal 'bar', cache['foo'] diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 02b67f901f..938350627f 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -500,7 +500,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase line_item = LineItem.create! invoice = Invoice.create!(line_items: [line_item]) initial = invoice.updated_at - line_item.touch + travel(1.second) do + line_item.touch + end assert_not_equal initial, invoice.reload.updated_at end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index f487065d9d..cd19a7a5bc 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -170,7 +170,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase part = ShipPart.create(name: 'cockpit') updated_at = part.updated_at - ship.parts << part + travel(1.second) do + ship.parts << part + end assert_equal part.ship, ship assert_not_equal part.updated_at, updated_at @@ -939,7 +941,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # option is not given on the association. ship = Ship.create(name: 'Countless', treasures_count: 10) - assert_not ship.treasures.instance_variable_get('@association').send(:has_cached_counter?) + assert_not Ship.reflect_on_association(:treasures).has_cached_counter? # Count should come from sql count() of treasures rather than treasures_count attribute assert_equal ship.treasures.size, 0 @@ -1482,6 +1484,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {companies: 'client companies'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + firm.destroy + + assert !firm.errors.empty? + + assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_included_in_collection assert_equal true, companies(:first_firm).clients.include?(Client.find(2)) end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index d46e7ad235..c9d9e29f09 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -219,6 +219,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {account: 'firm account'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + firm.destroy + + assert !firm.errors.empty? + assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_successful_build_association firm = Firm.new("name" => "GlobalMegaCorp") firm.save diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index e2608e3670..b67201d94d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -175,7 +175,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal category_attrs , category.attributes_before_type_cast end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_read_attributes_before_type_cast_on_boolean bool = Boolean.create({ "value" => false }) if RUBY_PLATFORM =~ /java/ diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 0ec368f51d..c69782d93f 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -4,7 +4,6 @@ module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do @type = Minitest::Mock.new - @type.expect(:==, false, [false]) end teardown do diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 0edb8a8901..991145bed4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - require "cases/helper" require 'models/post' require 'models/author' @@ -206,7 +204,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56? + if subsecond_precision_supported? assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -1299,9 +1297,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) - assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do + assert_raises NoMethodError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end end end @@ -1315,18 +1314,20 @@ class BasicsTest < ActiveRecord::TestCase error = e end - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do - exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message end - assert_equal error.message, exception.message end def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) - assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do + assert_raises ArgumentError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 80244d1439..2749273884 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -22,6 +22,10 @@ module ActiveRecord assert_lookup_type :string, "SET('one', 'two', 'three')" end + def test_set_type_with_value_matching_other_type + assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')" + end + def test_enum_type_with_value_matching_other_type assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index c7531f5418..db832fe55d 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -29,7 +29,7 @@ module ActiveRecord def test_clearing @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache.clear! @@ -40,17 +40,22 @@ module ActiveRecord def test_dump_and_load @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) assert_equal 11, @cache.columns('posts').size assert_equal 11, @cache.columns_hash('posts').size - assert @cache.tables('posts') + assert @cache.data_sources('posts') assert_equal 'id', @cache.primary_keys('posts') end + def test_table_methods_deprecation + assert_deprecated { assert @cache.table_exists?('posts') } + assert_deprecated { assert @cache.tables('posts') } + assert_deprecated { @cache.clear_table_cache!('posts') } + end end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index f5aaf22e13..e9a80842d0 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -467,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase topic.save! updated_at = topic.updated_at - topic.content[:hello] = 'world' - topic.save! + travel(1.second) do + topic.content[:hello] = 'world' + topic.save! + end assert_not_equal updated_at, topic.updated_at assert_equal 'world', topic.content[:hello] @@ -521,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase assert_equal Hash.new, pirate.previous_changes pirate = Pirate.find_by_catchphrase("arrr") + + travel(1.second) + pirate.catchphrase = "Me Maties!" pirate.save! @@ -532,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('created_on') pirate = Pirate.find_by_catchphrase("Me Maties!") + + travel(1.second) + pirate.catchphrase = "Thar She Blows!" pirate.save @@ -542,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Thar She Blows!") pirate.update(catchphrase: "Ahoy!") @@ -552,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Ahoy!") pirate.update_attribute(:catchphrase, "Ninjas suck!") @@ -561,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes['updated_on'][1] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + ensure + travel_back end if ActiveRecord::Base.connection.supports_migrations? diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index f1d5511bb8..64dfd86ce2 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -39,38 +39,49 @@ if ActiveRecord::Base.connection.supports_explain? binds = [[], []] queries = sqls.zip(binds) - connection.stubs(:explain).returns('query plan foo', 'query plan bar') - expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") - assert_equal expected, base.exec_explain(queries) + stub_explain_for_query_plans do + expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + assert_equal expected, base.exec_explain(queries) + end end def test_exec_explain_with_binds - cols = [Object.new, Object.new] - cols[0].expects(:name).returns('wadus') - cols[1].expects(:name).returns('chaflan') + object = Struct.new(:name) + cols = [object.new('wadus'), object.new('chaflan')] sqls = %w(foo bar) binds = [[[cols[0], 1]], [[cols[1], 2]]] queries = sqls.zip(binds) - connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n") - expected = <<-SQL.strip_heredoc - EXPLAIN for: #{sqls[0]} [["wadus", 1]] - query plan foo + stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do + expected = <<-SQL.strip_heredoc + EXPLAIN for: #{sqls[0]} [["wadus", 1]] + query plan foo - EXPLAIN for: #{sqls[1]} [["chaflan", 2]] - query plan bar - SQL - assert_equal expected, base.exec_explain(queries) + EXPLAIN for: #{sqls[1]} [["chaflan", 2]] + query plan bar + SQL + assert_equal expected, base.exec_explain(queries) + end end def test_unsupported_connection_adapter - connection.stubs(:supports_explain?).returns(false) + connection.stub(:supports_explain?, false) do + assert_not_called(base.logger, :warn) do + Car.where(:name => 'honda').to_a + end + end + end - base.logger.expects(:warn).never + private - Car.where(:name => 'honda').to_a - end + def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar']) + explain_called = 0 + + connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do + yield + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 4b819a82e8..307b68764e 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -178,8 +178,9 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_does_not_instantiate_records - Developer.expects(:instantiate).never - Developer.exists? + assert_not_called(Developer, :instantiate) do + Developer.exists? + end end def test_find_by_array_of_one_id @@ -700,12 +701,12 @@ class FinderTest < ActiveRecord::TestCase end def test_bind_arity - assert_nothing_raised { bind '' } + assert_nothing_raised { bind '' } assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } - assert_nothing_raised { bind '?', 1 } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + assert_nothing_raised { bind '?', 1 } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } end def test_named_bind_variables @@ -720,6 +721,12 @@ class FinderTest < ActiveRecord::TestCase assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end + def test_named_bind_arity + assert_nothing_raised { bind "name = :name", { name: "37signals" } } + assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } + end + class SimpleEnumerable include Enumerable diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 64759160dc..e3c4bd9f4a 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -408,9 +408,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase end def test_reloading_fixtures_through_accessor_methods + topic = Struct.new(:title) assert_equal "The First Topic", topics(:first).title - @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!")) - assert_equal "Fresh Topic!", topics(:first, true).title + assert_called(@loaded_fixtures['topics']['first'], :find, returns: topic.new("Fresh Topic!")) do + assert_equal "Fresh Topic!", topics(:first, true).title + end end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index b61a5126e0..8773986882 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -46,10 +46,10 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end -def mysql_56? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && - ActiveRecord::Base.connection.send(:version) >= '5.6.0' && - ActiveRecord::Base.connection.send(:version) < '5.7.0' +def subsecond_precision_supported? + !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || + (ActiveRecord::Base.connection.send(:version) >= '5.6.0' && + ActiveRecord::Base.connection.send(:version) < '5.7.0') end def mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 018b7b0d8f..80d17b8114 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -125,6 +125,7 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key dev.touch diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 4192d12ff4..3846ba8e7f 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity + REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR) + REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD) + REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA) + REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN) + SQL_COLORINGS = { + SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE), + INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN), + UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW), + DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED), + LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE), + ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED), + TRANSACTION: REGEXP_CYAN, + OTHER: REGEXP_MAGENTA + } class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -71,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_basic_query_logging_coloration + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, color_regex| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_generic_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_named_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_multi_line_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + sql = <<-EOS + #{verb} + WHERE ID IN ( + SELECT ID FROM THINGS + ) + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_lock + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + sql = <<-EOS + SELECT * FROM + (SELECT * FROM mytable FOR UPDATE) ss + WHERE col1 = 5; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + + sql = <<-EOS + LOCK TABLE films IN SHARE MODE; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + def test_exists_query_logging Developer.exists? 1 wait diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 99f1dc65b0..1e3529db54 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -31,7 +31,8 @@ module ActiveRecord end def test_unknown_commands_delegate - recorder = CommandRecorder.new(stub(:foo => 'bar')) + recorder = Struct.new(:foo) + recorder = CommandRecorder.new(recorder.new('bar')) assert_equal 'bar', recorder.foo end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index 5bc0898f33..ad85684c0b 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -28,7 +28,7 @@ module ActiveRecord super TestModel.reset_table_name TestModel.reset_sequence_name - connection.drop_table :test_models rescue nil + connection.drop_table :test_models, if_exists: true end private diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 1594f99852..4f0da999d8 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -32,6 +32,14 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("testings") end + test "foreign keys can be created in one query" do + assert_queries(1) do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + end + end + test "options hash can be passed" do @connection.change_table :testing_parents do |t| t.integer :other_id diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 128a242495..10f1c7216f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -75,15 +75,13 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal false, ActiveRecord::Migrator.needs_migration? ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? - ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version) + ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, ActiveRecord::Migrator.needs_migration? ensure ActiveRecord::Migrator.migrations_paths = old_path diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b8a0401fe3..93cb631a04 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -273,10 +273,11 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @ship.stubs(:id).returns('ABC1X') - @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } + @ship.stub(:id, 'ABC1X') do + @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } - assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -457,10 +458,11 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @pirate.stubs(:id).returns('ABC1X') - @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } + @pirate.stub(:id, 'ABC1X') do + @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } - assert_equal 'Arr', @ship.pirate.catchphrase + assert_equal 'Arr', @ship.pirate.catchphrase + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -638,17 +640,19 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models - @child_1.stubs(:id).returns('ABC1X') - @child_2.stubs(:id).returns('ABC2X') - - @pirate.attributes = { - association_getter => [ - { :id => @child_1.id, :name => 'Grace OMalley' }, - { :id => @child_2.id, :name => 'Privateers Greed' } - ] - } + @child_1.stub(:id, 'ABC1X') do + @child_2.stub(:id, 'ABC2X') do + + @pirate.attributes = { + association_getter => [ + { :id => @child_1.id, :name => 'Grace OMalley' }, + { :id => @child_2.id, :name => 'Privateers Greed' } + ] + } - assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + end + end end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 0745a37ee9..5e4ba47988 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -241,6 +241,33 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end end +class CompositePrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.integer :code + end + end + + def teardown + @connection.drop_table(:barcodes, if_exists: true) + end + + def test_composite_primary_key + assert_equal ["region", "code"], @connection.primary_keys("barcodes") + end + + def test_collectly_dump_composite_primary_key + schema = dump_table_schema "barcodes" + assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -300,11 +327,12 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) if current_adapter?(:MysqlAdapter, :Mysql2Adapter) test "primary key column type with options" do - @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true) + @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == 'id' } assert column.auto_increment? assert_equal :integer, column.type assert_equal 8, column.limit + assert column.unsigned? end end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index ba4d9d2503..88d2dd55ab 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -55,9 +55,10 @@ module ActiveRecord test '#order! on non-string does not attempt regexp match for references' do obj = Object.new - obj.expects(:=~).never - assert relation.order!(obj) - assert_equal [obj], relation.order_values + assert_not_called(obj, :=~) do + assert relation.order!(obj) + assert_equal [obj], relation.order_values + end end test '#references!' do diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 37d3965022..b0fb905760 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -243,18 +243,23 @@ module ActiveRecord end def test_select_quotes_when_using_from_clause - if sqlite3_version_includes_quoting_bug? - skip <<-ERROR.squish - You are using an outdated version of SQLite3 which has a bug in - quoted column names. Please update SQLite3 and rebuild the sqlite3 - ruby gem - ERROR - end + skip_if_sqlite3_version_includes_quoting_bug quoted_join = ActiveRecord::Base.connection.quote_table_name("join") selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join) assert_equal Post.pluck(:id), selected end + def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used + skip_if_sqlite3_version_includes_quoting_bug + klass = Class.new(ActiveRecord::Base) do + self.table_name = :test_with_keyword_column_name + alias_attribute :description, :desc + end + klass.create!(description: "foo") + + assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc) + end + def test_relation_merging_with_merged_joins_as_strings join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" special_comments_with_ratings = SpecialComment.joins join_string @@ -292,6 +297,16 @@ module ActiveRecord private + def skip_if_sqlite3_version_includes_quoting_bug + if sqlite3_version_includes_quoting_bug? + skip <<-ERROR.squish + You are using an outdated version of SQLite3 which has a bug in + quoted column names. Please update SQLite3 and rebuild the sqlite3 + ruby gem + ERROR + end + end + def sqlite3_version_includes_quoting_bug? if current_adapter?(:SQLite3Adapter) selected_quoted_column_names = ActiveRecord::Base.connection.exec_query( diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index d0deb4c273..a93fa57257 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -270,15 +270,16 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end - def test_warn_when_external_structure_dump_fails + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(false) + Kernel.expects(:system) + .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db") + .returns(false) - warnings = capture(:stderr) do + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - end - - assert_match(/Could not dump the database structure/, warnings) + } + assert_match(/^failed to execute: `mysqldump`$/, e.message) end def test_structure_dump_with_port_number @@ -311,6 +312,7 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") + .returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 7761ea5612..47e664f4e7 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -103,7 +103,7 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] + mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 3f4baf8378..1970fe82d0 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -28,7 +28,7 @@ class TestFixturesTest < ActiveRecord::TestCase assert_equal true, @klass.use_transactional_tests end - def test_use_transactional_tests_can_be_overriden + def test_use_transactional_tests_can_be_overridden @klass.use_transactional_tests = "foobar" assert_equal "foobar", @klass.use_transactional_tests diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 5dab32995c..970f6bcf4a 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -84,7 +84,9 @@ class TimestampTest < ActiveRecord::TestCase def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at - @developer.touch(:created_at) + travel(1.second) do + @developer.touch(:created_at) + end assert !@developer.created_at_changed? , 'created_at should not be changed' assert !@developer.changed?, 'record should not be changed' @@ -199,8 +201,10 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.name = "Fluffy the Third" - pet.save + travel(1.second) do + pet.name = "Fluffy the Third" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -210,7 +214,9 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.destroy + travel(1.second) do + pet.destroy + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -254,8 +260,10 @@ class TimestampTest < ActiveRecord::TestCase owner.update_columns(happy_at: 3.days.ago) previously_owner_updated_at = owner.updated_at - pet.name = "I'm a parrot" - pet.save + travel(1.second) do + pet.name = "I'm a parrot" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index b3c42c8e42..7058f4fbe2 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -72,7 +72,7 @@ class TouchLaterTest < ActiveRecord::TestCase end def test_touch_touches_immediately_with_a_custom_time - time = Time.now.utc - 25.days + time = (Time.now.utc - 25.days).change(nsec: 0) topic = Topic.create!(updated_at: time, created_at: time) assert_equal time, topic.updated_at assert_equal time, topic.created_at diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 29a6ec7522..ec5bdfd725 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -487,13 +487,17 @@ class TransactionTest < ActiveRecord::TestCase end def test_rollback_when_commit_raises - Topic.connection.expects(:begin_db_transaction) - Topic.connection.expects(:commit_db_transaction).raises('OH NOES') - Topic.connection.expects(:rollback_db_transaction) + assert_called(Topic.connection, :begin_db_transaction) do + Topic.connection.stub(:commit_db_transaction, ->{ raise('OH NOES') }) do + assert_called(Topic.connection, :rollback_db_transaction) do - assert_raise RuntimeError do - Topic.transaction do - # do nothing + e = assert_raise RuntimeError do + Topic.transaction do + # do nothing + end + end + assert_equal 'OH NOES', e.message + end end end end diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb new file mode 100644 index 0000000000..bc4900e1c2 --- /dev/null +++ b/activerecord/test/cases/type/date_time_test.rb @@ -0,0 +1,14 @@ +require "cases/helper" +require "models/task" + +module ActiveRecord + module Type + class IntegerTest < ActiveRecord::TestCase + def test_datetime_seconds_precision_applied_to_timestamp + skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported? + p = Task.create!(starting: ::Time.now) + assert_equal p.starting.usec, p.reload.starting.usec + end + end + end +end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 0dcdbd0667..c0932d5357 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -4,112 +4,12 @@ require "models/company" module ActiveRecord module Type class IntegerTest < ActiveRecord::TestCase - test "simple values" do - type = Type::Integer.new - assert_equal 1, type.cast(1) - assert_equal 1, type.cast('1') - assert_equal 1, type.cast('1ignore') - assert_equal 0, type.cast('bad1') - assert_equal 0, type.cast('bad') - assert_equal 1, type.cast(1.7) - assert_equal 0, type.cast(false) - assert_equal 1, type.cast(true) - assert_nil type.cast(nil) - end - - test "random objects cast to nil" do - type = Type::Integer.new - assert_nil type.cast([1,2]) - assert_nil type.cast({1 => 2}) - assert_nil type.cast(1..2) - end - test "casting ActiveRecord models" do type = Type::Integer.new firm = Firm.create(:name => 'Apple') assert_nil type.cast(firm) end - test "casting objects without to_i" do - type = Type::Integer.new - assert_nil type.cast(::Object.new) - end - - test "casting nan and infinity" do - type = Type::Integer.new - assert_nil type.cast(::Float::NAN) - assert_nil type.cast(1.0/0.0) - end - - test "casting booleans for database" do - type = Type::Integer.new - assert_equal 1, type.serialize(true) - assert_equal 0, type.serialize(false) - end - - test "changed?" do - type = Type::Integer.new - - assert type.changed?(5, 5, '5wibble') - assert_not type.changed?(5, 5, '5') - assert_not type.changed?(5, 5, '5.0') - assert_not type.changed?(-5, -5, '-5') - assert_not type.changed?(-5, -5, '-5.0') - assert_not type.changed?(nil, nil, nil) - end - - test "values below int min value are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(-2147483649) - end - end - - test "values above int max value are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(2147483648) - end - end - - test "very small numbers are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(-9999999999999999999999999999999) - end - end - - test "very large numbers are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(9999999999999999999999999999999) - end - end - - test "normal numbers are in range" do - type = Integer.new - assert_equal(0, type.serialize(0)) - assert_equal(-1, type.serialize(-1)) - assert_equal(1, type.serialize(1)) - end - - test "int max value is in range" do - assert_equal(2147483647, Integer.new.serialize(2147483647)) - end - - test "int min value is in range" do - assert_equal(-2147483648, Integer.new.serialize(-2147483648)) - end - - test "columns with a larger limit have larger ranges" do - type = Integer.new(limit: 8) - - assert_equal(9223372036854775807, type.serialize(9223372036854775807)) - assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) - assert_raises(::RangeError) do - type.serialize(-9999999999999999999999999999999) - end - assert_raises(::RangeError) do - type.serialize(9999999999999999999999999999999) - end - end - test "values which are out of range can be re-assigned" do klass = Class.new(ActiveRecord::Base) do self.table_name = 'posts' diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 56e9bf434d..6fe6d46711 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -2,20 +2,6 @@ require 'cases/helper' module ActiveRecord class StringTypeTest < ActiveRecord::TestCase - test "type casting" do - type = Type::String.new - assert_equal "t", type.cast(true) - assert_equal "f", type.cast(false) - assert_equal "123", type.cast(123) - end - - test "values are duped coming out" do - s = "foo" - type = Type::String.new - assert_not_same s, type.cast(s) - assert_not_same s, type.deserialize(s) - end - test "string mutations are detected" do klass = Class.new(Base) klass.table_name = 'authors' diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 9b1859c2ce..81fcf04a27 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -3,111 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class TypesTest < ActiveRecord::TestCase - def test_type_cast_boolean - type = Type::Boolean.new - assert type.cast('').nil? - assert type.cast(nil).nil? - - assert type.cast(true) - assert type.cast(1) - assert type.cast('1') - assert type.cast('t') - assert type.cast('T') - assert type.cast('true') - assert type.cast('TRUE') - assert type.cast('on') - assert type.cast('ON') - assert type.cast(' ') - assert type.cast("\u3000\r\n") - assert type.cast("\u0000") - assert type.cast('SOMETHING RANDOM') - - # explicitly check for false vs nil - assert_equal false, type.cast(false) - assert_equal false, type.cast(0) - assert_equal false, type.cast('0') - assert_equal false, type.cast('f') - assert_equal false, type.cast('F') - assert_equal false, type.cast('false') - assert_equal false, type.cast('FALSE') - assert_equal false, type.cast('off') - assert_equal false, type.cast('OFF') - end - - def test_type_cast_float - type = Type::Float.new - assert_equal 1.0, type.cast("1") - end - - def test_changing_float - type = Type::Float.new - - assert type.changed?(5.0, 5.0, '5wibble') - assert_not type.changed?(5.0, 5.0, '5') - assert_not type.changed?(5.0, 5.0, '5.0') - assert_not type.changed?(nil, nil, nil) - end - - def test_type_cast_binary - type = Type::Binary.new - assert_equal nil, type.cast(nil) - assert_equal "1", type.cast("1") - assert_equal 1, type.cast(1) - end - - def test_type_cast_time - type = Type::Time.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - type = Type::DateTime.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - type = Type::Date.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - type = Type::Integer.new - assert_equal 1800, type.cast(30.minutes) - assert_equal 7200, type.cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :local].each do |zone| - with_timezone_config default: zone do - type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - def test_type_equality - assert_equal Type::Value.new, Type::Value.new - assert_not_equal Type::Value.new, Type::Integer.new - assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) - end - def test_attributes_which_are_invalid_for_database_can_still_be_reassigned type_which_cannot_go_to_the_database = Type::Value.new def type_which_cannot_go_to_the_database.serialize(*) diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index f95f8f0b8f..c5d8f8895c 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'models/owner' require 'models/pet' diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index f4f316f393..a429d06aad 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) end + def test_invalid_using_multiple_contexts + r = WrongReply.new(:title => 'Wrong Create') + assert r.invalid?([:special_case, :create]) + assert_equal "Invalid", r.errors[:author_name].join + assert_equal "is Wrong Create", r.errors[:title].join + end + def test_validate r = WrongReply.new diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 1eb1430065..e80d8bd584 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -1,7 +1,9 @@ require "cases/helper" require "models/book" +require "support/schema_dumping_helper" module ViewBehavior + include SchemaDumpingHelper extend ActiveSupport::Concern included do @@ -31,11 +33,26 @@ module ViewBehavior assert_equal ["Ruby for Rails"], books.map(&:name) end + def test_views + assert_equal [Ebook.table_name], @connection.views + end + + def test_view_exists + view_name = Ebook.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Ebook.table_name + # TODO: switch this assertion around once we changed #tables to not return views. assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" end + def test_views_ara_valid_data_sources + view_name = Ebook.table_name + assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source" + end + def test_column_definitions assert_equal([["id", :integer], ["name", :string], @@ -53,6 +70,11 @@ module ViewBehavior end assert_nil model.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "ebooks" + assert_no_match %r{create_table "ebooks"}, schema + end end if ActiveRecord::Base.connection.supports_views? @@ -70,6 +92,7 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper fixtures :books class Paperback < ActiveRecord::Base; end @@ -91,6 +114,15 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase assert_equal ["Agile Web Development with Rails"], books.map(&:name) end + def test_views + assert_equal [Paperback.table_name], @connection.views + end + + def test_view_exists + view_name = Paperback.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Paperback.table_name assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" @@ -109,6 +141,11 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase def test_does_not_have_a_primary_key assert_nil Paperback.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "paperbacks" + assert_no_match %r{create_table "paperbacks"}, schema + end end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 3ea17c3abf..9f2f69e1ee 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -3,7 +3,7 @@ module ContactFakeColumns base.class_eval do establish_connection(:adapter => 'fake') - connection.tables = [table_name] + connection.data_sources = [table_name] connection.primary_keys = { table_name => 'id' } diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 994ece9244..4c6d1ef3ce 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,4 +1,3 @@ - ActiveRecord::Schema.define do def except(adapter_names_to_exclude) unless [adapter_names_to_exclude].flatten.include?(adapter_name) @@ -252,10 +251,17 @@ ActiveRecord::Schema.define do create_table :developers, force: true do |t| t.string :name t.integer :salary, default: 70000 - t.datetime :created_at - t.datetime :updated_at - t.datetime :created_on - t.datetime :updated_on + if subsecond_precision_supported? + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + t.datetime :created_on, precision: 6 + t.datetime :updated_on, precision: 6 + else + t.datetime :created_at + t.datetime :updated_at + t.datetime :created_on + t.datetime :updated_on + end end create_table :developers_projects, force: true, id: false do |t| @@ -354,7 +360,11 @@ ActiveRecord::Schema.define do create_table :invoices, force: true do |t| t.integer :balance - t.datetime :updated_at + if subsecond_precision_supported? + t.datetime :updated_at, precision: 6 + else + t.datetime :updated_at + end end create_table :iris, force: true do |t| @@ -504,7 +514,11 @@ ActiveRecord::Schema.define do create_table :owners, primary_key: :owner_id, force: true do |t| t.string :name - t.column :updated_at, :datetime + if subsecond_precision_supported? + t.column :updated_at, :datetime, precision: 6 + else + t.column :updated_at, :datetime + end t.column :happy_at, :datetime t.string :essay_id end @@ -522,10 +536,17 @@ ActiveRecord::Schema.define do t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer - t.column :created_at, :datetime - t.column :created_on, :datetime - t.column :updated_at, :datetime - t.column :updated_on, :datetime + if subsecond_precision_supported? + t.column :created_at, :datetime, precision: 0 + t.column :created_on, :datetime, precision: 0 + t.column :updated_at, :datetime, precision: 0 + t.column :updated_on, :datetime, precision: 0 + else + t.column :created_at, :datetime + t.column :created_on, :datetime + t.column :updated_at, :datetime + t.column :updated_on, :datetime + end end create_table :parrots_pirates, id: false, force: true do |t| @@ -568,15 +589,24 @@ ActiveRecord::Schema.define do create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - t.timestamps null: false + if subsecond_precision_supported? + t.timestamps null: false, precision: 6 + else + t.timestamps null: false + end end create_table :pirates, force: true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer t.integer :non_validated_parrot_id - t.column :created_on, :datetime - t.column :updated_on, :datetime + if subsecond_precision_supported? + t.column :created_on, :datetime, precision: 6 + t.column :updated_on, :datetime, precision: 6 + else + t.column :created_on, :datetime + t.column :updated_on, :datetime + end end create_table :posts, force: true do |t| @@ -686,7 +716,11 @@ ActiveRecord::Schema.define do create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id - t.datetime :updated_at + if subsecond_precision_supported? + t.datetime :updated_at, precision: 6 + else + t.datetime :updated_at + end end create_table :prisoners, force: true do |t| @@ -756,7 +790,7 @@ ActiveRecord::Schema.define do t.string :title, limit: 250 t.string :author_name t.string :author_email_address - if mysql_56? + if subsecond_precision_supported? t.datetime :written_on, precision: 6 else t.datetime :written_on @@ -779,7 +813,11 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - t.timestamps null: true + if subsecond_precision_supported? + t.timestamps null: true, precision: 6 + else + t.timestamps null: true + end end create_table :toys, primary_key: :toy_id, force: true do |t| @@ -944,6 +982,10 @@ ActiveRecord::Schema.define do t.string :token t.string :auth_token end + + create_table :test_with_keyword_column_name, force: true do |t| + t.string :desc + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index addf5d521a..309d842da1 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,14 @@ +* Short-circuit `blank?` on date and time values since they are never blank. + + Fixes #21657 + + *Andrew White* + +* Replaced deprecated `ThreadSafe::Cache` with its successor `Concurrent::Map` now that + the thread_safe gem has been merged into concurrent-ruby. + + *Jerry D'Antonio* + * Updated Unicode version to 8.0.0 *Anshul Sharma* @@ -266,20 +277,17 @@ The preferred method to halt a callback chain from now on is to explicitly `throw(:abort)`. - In the past, returning `false` in an ActiveSupport callback had the side - effect of halting the callback chain. This is not recommended anymore and, - depending on the value of - `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will - either not work at all or display a deprecation warning. + In the past, callbacks could only be halted by explicitly providing a + terminator and by having a callback match the conditions of the terminator. * Add `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` - to `true` will let an app support the deprecated way of halting callback - chains by returning `false`. + to `true` will let an app support the deprecated way of halting Active Record, + Active Model and Active Model validations callback chains by returning `false`. Setting the value to `false` will tell the app to ignore any `false` value - returned by callbacks, and only halt the chain upon `throw(:abort)`. + returned by those callbacks, and only halt the chain upon `throw(:abort)`. The value can also be set with the Rails configuration option `config.active_support.halt_callback_chains_on_return_false`. @@ -289,7 +297,7 @@ For new Rails 5.0 apps, its value is set to `false` in an initializer, so these apps will support the new behavior by default. - *claudiob* + *claudiob*, *Roque Pinel* * Changes arguments and default value of CallbackChain's `:terminator` option diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 67b6663915..d236dbd200 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -24,7 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' - s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4' - s.add_dependency 'concurrent-ruby', '~> 0.9.1' + s.add_dependency 'concurrent-ruby', '~> 1.0.0.pre2', '< 2.0.0' s.add_dependency 'method_source' end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index fefba5b0fd..3db9ea2ac0 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -126,14 +126,10 @@ module ActiveSupport def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter) halted_lambda = chain_config[:terminator] - if chain_config.key?(:terminator) && user_conditions.any? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback, halted_lambda, filter) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback, halted_lambda, filter) end end @@ -175,42 +171,15 @@ module ActiveSupport end end private_class_method :halting - - def self.conditional(callback_sequence, user_callback, user_conditions) - callback_sequence.before do |env| - target = env.target - value = env.value - - if user_conditions.all? { |c| c.call(target, value) } - user_callback.call target, value - end - - env - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.before do |env| - user_callback.call env.target, env.value - - env - end - end - private_class_method :simple end class After def self.build(callback_sequence, user_callback, user_conditions, chain_config) if chain_config[:skip_after_callbacks_if_terminated] - if chain_config.key?(:terminator) && user_conditions.any? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key?(:terminator) - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional callback_sequence, user_callback, user_conditions else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback) end else if user_conditions.any? @@ -273,14 +242,10 @@ module ActiveSupport class Around def self.build(callback_sequence, user_callback, user_conditions, chain_config) - if chain_config.key?(:terminator) && user_conditions.any? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple(callback_sequence, user_callback) + halting(callback_sequence, user_callback) end end @@ -318,33 +283,6 @@ module ActiveSupport end end private_class_method :halting - - def self.conditional(callback_sequence, user_callback, user_conditions) - callback_sequence.around do |env, &run| - target = env.target - value = env.value - - if user_conditions.all? { |c| c.call(target, value) } - user_callback.call(target, value) { - run.call.value - } - env - else - run.call - end - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.around do |env, &run| - user_callback.call(env.target, env.value) { - run.call.value - } - env - end - end - private_class_method :simple end end @@ -598,23 +536,12 @@ module ActiveSupport Proc.new do |target, result_lambda| terminate = true catch(:abort) do - result = result_lambda.call if result_lambda.is_a?(Proc) - if halt_and_display_warning_on_return_false && result == false - display_deprecation_warning_for_false_terminator - else - terminate = false - end + result_lambda.call if result_lambda.is_a?(Proc) + terminate = false end terminate end end - - def display_deprecation_warning_for_false_terminator - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails. - To explicitly halt a callback chain, please use `throw :abort` instead. - MSG - end end module ClassMethods @@ -748,7 +675,8 @@ module ActiveSupport # # In this example, if any before validate callbacks returns +false+, # any successive before and around callback is not executed. - # Defaults to +false+, meaning no value halts the chain. + # + # The default terminator halts the chain when a callback throws +:abort+. # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By @@ -826,6 +754,30 @@ module ActiveSupport def set_callbacks(name, callbacks) send "_#{name}_callbacks=", callbacks end + + def deprecated_false_terminator + Proc.new do |target, result_lambda| + terminate = true + catch(:abort) do + result = result_lambda.call if result_lambda.is_a?(Proc) + if CallbackChain.halt_and_display_warning_on_return_false && result == false + display_deprecation_warning_for_false_terminator + else + terminate = false + end + end + terminate + end + end + + private + + def display_deprecation_warning_for_false_terminator + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails. + To explicitly halt the callback chain, please use `throw :abort` instead. + MSG + end end end end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index d80df21e7d..8718b7e1e5 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -32,7 +32,7 @@ class Array # ['one', 'two', 'three'].to_sentence # => "one, two, and three" # # ['one', 'two'].to_sentence(passing: 'invalid option') - # # => ArgumentError: Unknown key :passing + # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale # # ['one', 'two'].to_sentence(two_words_connector: '-') # # => "one-two" @@ -85,7 +85,9 @@ class Array # Extends <tt>Array#to_s</tt> to convert a collection of elements into a # comma separated id list if <tt>:db</tt> argument is given as the format. # - # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.none.to_formatted_s(:db) # => "null" + # [1,2].to_formatted_s # => "[1, 2]" def to_formatted_s(format = :default) case format when :db diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb index 465fedda80..7f0f4639a2 100644 --- a/activesupport/lib/active_support/core_ext/date.rb +++ b/activesupport/lib/active_support/core_ext/date.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/date/acts_like' +require 'active_support/core_ext/date/blank' require 'active_support/core_ext/date/calculations' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/date/zones' - diff --git a/activesupport/lib/active_support/core_ext/date/blank.rb b/activesupport/lib/active_support/core_ext/date/blank.rb new file mode 100644 index 0000000000..71627b6a6f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class Date #:nodoc: + # No Date is blank: + # + # Date.today.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 31479a1269..ed8bca77ac 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -75,10 +75,10 @@ class Date # # date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007 # - # date.to_time # => Sat Nov 10 00:00:00 0800 2007 - # date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007 + # date.to_time # => 2007-11-10 00:00:00 0800 + # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # - # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007 + # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC def to_time(form = :local) ::Time.send(form, year, month, day) end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb index 96c6df9407..d29a8db5cf 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb @@ -4,7 +4,7 @@ module DateAndTime # if Time.zone_default is set. Otherwise, it returns the current time. # # Time.zone = 'Hawaii' # => 'Hawaii' - # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 # # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone @@ -14,7 +14,6 @@ module DateAndTime # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. # # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 def in_time_zone(zone = ::Time.zone) time_zone = ::Time.find_zone! zone diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb index e8a27b9f38..bcb228b09a 100644 --- a/activesupport/lib/active_support/core_ext/date_time.rb +++ b/activesupport/lib/active_support/core_ext/date_time.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/date_time/acts_like' +require 'active_support/core_ext/date_time/blank' require 'active_support/core_ext/date_time/calculations' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date_time/zones' diff --git a/activesupport/lib/active_support/core_ext/date_time/blank.rb b/activesupport/lib/active_support/core_ext/date_time/blank.rb new file mode 100644 index 0000000000..56981b75fb --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class DateTime #:nodoc: + # No DateTime is ever blank: + # + # DateTime.now.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 948d647875..463fd78412 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -10,7 +10,7 @@ class File # # This method needs to create a temporary file. By default it will create it # in the same directory as the destination file. If you don't like this - # behaviour you can provide a different directory but it must be on the + # behavior you can provide a different directory but it must be on the # same physical filesystem as the file you're trying to write. # # File.atomic_write('/data/something.important', '/data/tmp') do |file| 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 a77da573fe..a084177b9f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -19,9 +19,9 @@ class Module # The attribute name must be a valid method name in Ruby. # # module Foo - # mattr_reader :"1_Badname " + # mattr_reader :"1_Badname" # end - # # => NameError: invalid attribute name + # # => NameError: invalid attribute name: 1_Badname # # If you want to opt out the creation on the instance reader method, pass # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 0c8ff79237..d5bb11deed 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -41,7 +41,7 @@ class Numeric # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000% # 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399% # 1000.to_s(:percentage, locale: :fr) # => 1 000,000% - # 100.to_s(:percentage, format: '%n %') # => 100 % + # 100.to_s(:percentage, format: '%n %') # => 100.000 % # # Delimited: # 12345678.to_s(:delimited) # => 12,345,678 @@ -78,7 +78,7 @@ class Numeric # 1234567.to_s(:human_size, precision: 2) # => 1.2 MB # 483989.to_s(:human_size, precision: 2) # => 470 KB # 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1229 TB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" # 524288000.to_s(:human_size, precision: 5) # => "500 MB" # # Human-friendly format: diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 548c91638c..039c50a4a2 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - class Object # An object is blank if it's false, empty, or a whitespace string. # For example, +false+, '', ' ', +nil+, [], and {} are all blank. @@ -129,3 +127,14 @@ class Numeric #:nodoc: false end end + +class Time #:nodoc: + # No Time is blank: + # + # Time.now.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 55f281b213..d4c17dfb07 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -5,7 +5,7 @@ class Object # characters = ["Konata", "Kagami", "Tsukasa"] # "Konata".in?(characters) # => true # - # This will throw an ArgumentError if the argument doesn't respond + # This will throw an +ArgumentError+ if the argument doesn't respond # to +#include?+. def in?(another_object) another_object.include?(self) @@ -18,7 +18,7 @@ class Object # # params[:bucket_type].presence_in %w( project calendar ) # - # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+. # # @return [Object] def presence_in(another_object) diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index c67eb25b68..8c16d95b62 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -94,7 +94,7 @@ class Object # :call-seq: # try!(*a, &b) # - # Same as #try, but raises a NoMethodError exception if the receiver is + # Same as #try, but raises a +NoMethodError+ exception if the receiver is # not +nil+ and does not implement the tried method. # # "a".try!(:upcase) # => "A" diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index 6cdbea1f37..98cf7430f7 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -10,8 +10,8 @@ module SecureRandom # # The result may contain alphanumeric characters except 0, O, I and l # - # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" - # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 3e0cb8a7ac..fd79a40e31 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -14,7 +14,7 @@ class String # "06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100 - # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC + # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC # "12/13/2012".to_time # => ArgumentError: argument out of range def to_time(form = :local) parts = Date._parse(self, false) diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 7055f7f699..cc6f2158e7 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -9,12 +9,10 @@ class String # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string. # - # name = 'Claus Müller' - # name.reverse # => "rell??M sualC" - # name.length # => 13 - # - # name.mb_chars.reverse.to_s # => "rellüM sualC" - # name.mb_chars.length # => 12 + # >> "lj".upcase + # => "lj" + # >> "lj".mb_chars.upcase.to_s + # => "LJ" # # == Method chaining # diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index c676b26b06..8b27ec4413 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -86,7 +86,7 @@ class ERB # use inside HTML attributes. # # If your JSON is being used downstream for insertion into the DOM, be aware of - # whether or not it is being inserted via +html()+. Most JQuery plugins do this. + # whether or not it is being inserted via +html()+. Most jQuery plugins do this. # If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated # content returned by your JSON. # diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index 9fdd9d8d2e..55b9b87352 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - class String # Strips indentation in heredocs. # @@ -20,7 +18,6 @@ class String # Technically, it looks for the least indented non-empty line # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0 - gsub(/^[ \t]{#{indent}}/, '') + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index 0b2ff817c3..c6c183edd9 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'uri' str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. parser = URI::Parser.new diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 8215a3085e..16b726bcba 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -1,6 +1,6 @@ require 'set' require 'thread' -require 'thread_safe' +require 'concurrent' require 'pathname' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' @@ -585,7 +585,7 @@ module ActiveSupport #:nodoc: class ClassCache def initialize - @store = ThreadSafe::Cache.new + @store = Concurrent::Map.new end def empty? diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 42560f3515..c3907e9c22 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent' require 'active_support/core_ext/array/prepend_and_append' require 'active_support/i18n' @@ -25,7 +25,7 @@ module ActiveSupport # singularization rules that is runs. This guarantees that your rules run # before any of the rules that may already have been loaded. class Inflections - @__instance__ = ThreadSafe::Cache.new + @__instance__ = Concurrent::Map.new class Uncountables < Array def initialize diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index ab5372ac43..595b0339cc 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'active_support/inflections' module ActiveSupport diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 7b28eeb6e2..7472d4386a 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/core_ext/string/multibyte' require 'active_support/i18n' @@ -76,15 +75,15 @@ module ActiveSupport parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep) unless sep.nil? || sep.empty? if sep == "-".freeze - re_duplicate_seperator = /-{2,}/ + re_duplicate_separator = /-{2,}/ re_leading_trailing_separator = /^-|-$/i else re_sep = Regexp.escape(sep) - re_duplicate_seperator = /#{re_sep}{2,}/ + re_duplicate_separator = /#{re_sep}{2,}/ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i end # No more than one of the separator in a row. - parameterized_string.gsub!(re_duplicate_seperator, sep) + parameterized_string.gsub!(re_duplicate_separator, sep) # Remove leading/trailing separator. parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 51d2da3a79..6bc3db6ec6 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent' require 'openssl' module ActiveSupport @@ -28,7 +28,7 @@ module ActiveSupport class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator - @cache_keys = ThreadSafe::Cache.new + @cache_keys = Concurrent::Map.new end # Returns a derived key suitable for use. The default key_size is chosen diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e95dc5a866..e782cd2d4b 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -95,7 +95,7 @@ module ActiveSupport METHOD end - # Set color by using a string or one of the defined constants. If a third + # Set color by using a symbol or one of the defined constants. If a third # option is set to +true+, it also adds bold to the string. This is based # on the Highline implementation and will automatically append CLEAR to the # end of the returned String. diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 45cf6fc1ef..f6a2e7e949 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/json' require 'active_support/core_ext/string/access' require 'active_support/core_ext/string/behavior' diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 3f6a4c8457..5221a6dfb8 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module ActiveSupport module Multibyte module Unicode diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 0131fe2572..71354dd15f 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -1,5 +1,5 @@ require 'mutex_m' -require 'thread_safe' +require 'concurrent' module ActiveSupport module Notifications @@ -12,7 +12,7 @@ module ActiveSupport def initialize @subscribers = [] - @listeners_for = ThreadSafe::Cache.new + @listeners_for = Concurrent::Map.new super end @@ -51,7 +51,7 @@ module ActiveSupport end def listeners_for(name) - # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics) + # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics) @listeners_for[name] || synchronize do # use synchronisation when accessing @subscribers @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 9762d95145..504f96961a 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -118,7 +118,7 @@ module ActiveSupport # number_to_percentage(1000, locale: :fr) # => 1 000,000% # number_to_percentage:(1000, precision: nil) # => 1000% # number_to_percentage('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100 % + # number_to_percentage(100, format: '%n %') # => 100.000 % def number_to_percentage(number, options = {}) NumberToPercentageConverter.convert(number, options) end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 817522677f..eb26eb5ee5 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -132,7 +132,7 @@ module ActiveSupport # Returns a string of the object's date, time, zone and offset from UTC. # - # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00" + # Time.zone.now.inspect # => "Thu, 04 Dec 2014 11:00:25 EST -05:00" def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end @@ -388,6 +388,11 @@ module ActiveSupport end alias_method :kind_of?, :is_a? + # An instance of ActiveSupport::TimeWithZone is never blank + def blank? + false + end + def freeze period; utc; time # preload instance variables before freezing super diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 2699a064d7..4502e01b12 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,5 +1,5 @@ require 'tzinfo' -require 'thread_safe' +require 'concurrent' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' @@ -23,7 +23,7 @@ module ActiveSupport # config.time_zone = 'Eastern Time (US & Canada)' # end # - # Time.zone # => #<TimeZone:0x514834...> + # Time.zone # => #<ActiveSupport::TimeZone:0x514834...> # Time.zone.name # => "Eastern Time (US & Canada)" # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 # @@ -189,13 +189,13 @@ module ActiveSupport UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') - @lazy_zones_map = ThreadSafe::Cache.new + @lazy_zones_map = Concurrent::Map.new class << self # Assumes self represents an offset from UTC in seconds (as returned from # Time#utc_offset) and turns this into an +HH:MM formatted string. # - # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" + # ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" def seconds_to_utc_offset(seconds, colon = true) format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON sign = (seconds < 0 ? '-' : '+') diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index cda9732cae..4f47e0519f 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -766,13 +766,11 @@ module CallbacksTest end class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase - def test_returning_false_halts_callback_if_config_variable_is_not_set + def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set obj = CallbackFalseTerminator.new - assert_deprecated do - obj.save - assert_equal :second, obj.halted - assert !obj.saved - end + obj.save + assert_equal nil, obj.halted + assert obj.saved end end @@ -781,13 +779,11 @@ module CallbacksTest ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true end - def test_returning_false_halts_callback_if_config_variable_is_true + def test_returning_false_does_not_halt_callback_if_config_variable_is_true obj = CallbackFalseTerminator.new - assert_deprecated do - obj.save - assert_equal :second, obj.halted - assert !obj.saved - end + obj.save + assert_equal nil, obj.halted + assert obj.saved end end diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 8a5e385dd7..a142096993 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'active_support/core_ext/object/blank' diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index 5ea0f0eca6..25bf0207b8 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -121,7 +121,7 @@ class ObjectTryTest < ActiveSupport::TestCase assert_equal 5, Decorator.new(@string).size end - def test_try_with_overriden_method_on_delegator + def test_try_with_overridden_method_on_delegator assert_equal 'overridden reverse', Decorator.new(@string).reverse end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 18a8b92eb9..e6898658b5 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -1,4 +1,3 @@ - module InflectorTestCases SingularToPlural = { "search" => "searches", diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index d8704716e7..2a885e32bf 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' require 'multibyte_test_helpers' diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb index 11f5374017..360cf57302 100644 --- a/activesupport/test/multibyte_proxy_test.rb +++ b/activesupport/test/multibyte_proxy_test.rb @@ -1,4 +1,3 @@ - require 'abstract_unit' class MultibyteProxyText < ActiveSupport::TestCase diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 2e4b5cc873..58cf5488cd 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,4 +1,3 @@ - module MultibyteTestHelpers UNICODE_STRING = 'こにちわ'.freeze ASCII_STRING = 'ohayo'.freeze diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 17035069d0..69c7cd5136 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'redcarpet' require 'nokogiri' require 'rails_guides/markdown/renderer' diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 67f5f1cdd5..f50bcddbe7 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -16,9 +16,9 @@ <% end %> <p> The guides for earlier releases: -<a href="http://guides.rubyonrails.org/v4.2.0/">Rails 4.2.0</a>, -<a href="http://guides.rubyonrails.org/v4.1.8/">Rails 4.1.8</a>, -<a href="http://guides.rubyonrails.org/v4.0.12/">Rails 4.0.12</a>, -<a href="http://guides.rubyonrails.org/v3.2.21/">Rails 3.2.21</a> and -<a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. +<a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>, +<a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>, +<a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>, +<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and +<a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 19bdea2b8a..7e43ba375a 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -810,7 +810,7 @@ The [Security Guide](security.html) has more about this and a lot of other secur The Request and Response Objects -------------------------------- -In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `AbstractRequest` and the `response` method returns a response object representing what is going to be sent back to the client. +In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `ActionDispatch::Request` and the `response` method returns a response object representing what is going to be sent back to the client. ### The `request` Object @@ -1118,7 +1118,7 @@ Rails default exception handling displays a "500 Server Error" message for all e ### The Default 500 and 404 Templates -By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML. +By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them. ### `rescue_from` diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index c39cd34e9a..4800cece82 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -760,8 +760,8 @@ config.action_mailer.smtp_settings = { 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. +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 'smtp.gmail.com' above with the address of your provider. Mailer Testing -------------- diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 00c41a480e..76454e77c7 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -147,6 +147,39 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do end ``` +#### Jbuilder +[Jbuilder](https://github.com/rails/jbuilder) is a gem that's +maintained by the Rails team and included in the default Rails Gemfile. +It's similar to Builder, but is used to generate JSON, instead of XML. + +If you don't have it, you can add the following to your Gemfile: + +```ruby +gem 'jbuilder' +``` + +A Jbuilder object named `json` is automatically made available to templates with +a `.jbuilder` extension. + +Here is a basic example: + +```ruby +json.name("Alex") +json.email("alex@example.com") +``` + +would produce: + +```json +{ + "name": "Alex", + "email: "alex@example.com" +} +``` + +See the [Jbuilder documention](https://github.com/rails/jbuilder#jbuilder) for +more examples and information. + #### Template Caching By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index c0a4a0ba39..8ea0f383c0 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -10,7 +10,7 @@ After reading this guide, you will know: * How to find records using a variety of methods and conditions. * How to specify the order, retrieved attributes, grouping, and other properties of the found records. * How to use eager loading to reduce the number of database queries needed for data retrieval. -* How to use dynamic finders methods. +* How to use dynamic finder methods. * How to use method chaining to use multiple ActiveRecord methods together. * How to check for the existence of particular records. * How to perform various calculations on Active Record models. @@ -392,7 +392,7 @@ Now what if that number could vary, say as an argument from somewhere? The find Client.where("orders_count = ?", params[:orders]) ``` -Active Record will go through the first element in the conditions value and any additional elements will replace the question marks `(?)` in the first element. +Active Record will take the first argument as the conditions string and any additional arguments will replace the question marks `(?)` in it. If you want to specify multiple conditions: @@ -420,7 +420,7 @@ TIP: For more information on the dangers of SQL injection, see the [Ruby on Rail #### Placeholder Conditions -Similar to the `(?)` replacement style of params, you can also specify keys/values hash in your array conditions: +Similar to the `(?)` replacement style of params, you can also specify keys in your conditions string along with a corresponding keys/values hash: ```ruby Client.where("created_at >= :start_date AND created_at <= :end_date", @@ -431,7 +431,7 @@ This makes for clearer readability if you have a large number of variable condit ### Hash Conditions -Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: +Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want qualified and the values of how you want to qualify them: NOTE: Only equality, range and subset checking are possible with Hash conditions. @@ -531,7 +531,7 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") ``` -If you want to call `order` multiple times e.g. in different context, new order will append previous one: +If you want to call `order` multiple times, subsequent orders will be appended to the first: ```ruby Client.order("orders_count ASC").order("created_at DESC") @@ -619,9 +619,9 @@ SELECT * FROM clients LIMIT 5 OFFSET 30 Group ----- -To apply a `GROUP BY` clause to the SQL fired by the finder, you can specify the `group` method on the find. +To apply a `GROUP BY` clause to the SQL fired by the finder, you can use the `group` method. -For example, if you want to find a collection of the dates orders were created on: +For example, if you want to find a collection of the dates on which orders were created: ```ruby Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") @@ -639,7 +639,7 @@ GROUP BY date(created_at) ### Total of grouped items -To get the total of grouped items on a single query call `count` after the `group`. +To get the total of grouped items on a single query, call `count` after the `group`. ```ruby Order.group(:status).count @@ -675,7 +675,7 @@ GROUP BY date(created_at) HAVING sum(price) > 100 ``` -This will return single order objects for each day, but only those that are ordered more than $100 in a day. +This returns the date and total price for each order object, grouped by the day they were ordered and where the price is more than $100. Overriding Conditions --------------------- @@ -705,8 +705,7 @@ Article.where(id: 10, trashed: false).unscope(where: :id) # SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` -A relation which has used `unscope` will affect any relation it is -merged in to: +A relation which has used `unscope` will affect any relation into which it is merged: ```ruby Article.order('id asc').merge(Article.unscope(:order)) @@ -750,7 +749,7 @@ SELECT * FROM articles WHERE id = 10 SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` -In case the `reorder` clause is not used, the SQL executed would be: +In the case where the `reorder` clause is not used, the SQL executed would be: ```sql SELECT * FROM articles WHERE id = 10 @@ -839,7 +838,7 @@ end Readonly Objects ---------------- -Active Record provides `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. +Active Record provides the `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. ```ruby client = Client.readonly.first @@ -1052,7 +1051,7 @@ SELECT categories.* FROM categories ### Specifying Conditions on the Joined Tables -You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables: +You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provide a special syntax for specifying conditions for the joined tables: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight @@ -1091,7 +1090,7 @@ This code looks fine at the first sight. But the problem lies within the total n Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the `includes` method of the `Model.find` call. With `includes`, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries. -Revisiting the above case, we could rewrite `Client.limit(10)` to use eager load addresses: +Revisiting the above case, we could rewrite `Client.limit(10)` to eager load addresses: ```ruby clients = Client.includes(:address).limit(10) @@ -1428,7 +1427,7 @@ It's common that you need to find a record or create it if it doesn't exist. You ### `find_or_create_by` -The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. Let's see an example. +The `find_or_create_by` method checks whether a record with the specified attributes exists. If it doesn't, then `create` is called. Let's see an example. Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running: @@ -1868,6 +1867,6 @@ following pointers may be helpful: * SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html) -* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html) +* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html) * PostgreSQL: [Using EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html) diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index b99113ed3e..7f88c13dc0 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -242,7 +242,7 @@ end >> person = Person.new >> person.valid? ->> person.errors.details[:name] #=> [{error: :blank}] +>> person.errors.details[:name] # => [{error: :blank}] ``` Using `details` with custom validators is covered in the [Working with @@ -273,9 +273,13 @@ available helpers. This method validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your application's terms of service, confirm that some text is read, or any similar -concept. This validation is very specific to web applications and this -'acceptance' does not need to be recorded anywhere in your database (if you -don't have a field for it, the helper will just create a virtual attribute). +concept. + +This validation is very specific to web applications and this +'acceptance' does not need to be recorded anywhere in your database. If you +don't have a field for it, the helper will just create a virtual attribute. If +the field does exist in your database, the `accept` option must be set to +`true` or else the validation will not run. ```ruby class Person < ActiveRecord::Base @@ -636,7 +640,7 @@ class Holiday < ActiveRecord::Base message: "should happen once per year" } end ``` -Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns. +Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns. There is also a `:case_sensitive` option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 01bf928407..367a1bf7c0 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1865,15 +1865,15 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr ```ruby "2010-07-27".to_date # => Tue, 27 Jul 2010 -"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 +"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200 "2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 ``` `to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in: ```ruby -"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 -"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 +"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC +"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200 ``` Default is `:utc`. diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 28727a51bd..feaaff166a 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -194,7 +194,6 @@ An API application comes with the following middlewares by default: - `ActionDispatch::RemoteIp` - `ActionDispatch::Reloader` - `ActionDispatch::Callbacks` -- `ActionDispatch::ParamsParser` - `Rack::Head` - `Rack::ConditionalGet` - `Rack::ETag` @@ -292,9 +291,9 @@ instructions in the `Rack::Sendfile` documentation. NOTE: The `Rack::Sendfile` middleware is always outside of the `Rack::Lock` mutex, even in single-threaded applications. -### Using ActionDispatch::ParamsParser +### Using ActionDispatch::Request -`ActionDispatch::ParamsParser` will take parameters from the client in the JSON +`ActionDispatch::Request#params` will take parameters from the client in the JSON format and make them available in your controller inside `params`. To use this, your client will need to make a request with JSON-encoded parameters @@ -313,7 +312,7 @@ jQuery.ajax({ }); ``` -`ActionDispatch::ParamsParser` will see the `Content-Type` and your parameters +`ActionDispatch::Request` will see the `Content-Type` and your parameters will be: ```ruby diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index a4feff798d..526bf768cc 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -239,7 +239,7 @@ You can quickly test the RDoc output with the following command: ``` $ echo "+:to_param+" | rdoc --pipe -#=> <p><code>:to_param</code></p> +# => <p><code>:to_param</code></p> ``` ### Regular Font diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 1191f5edfe..60790b33a4 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -768,7 +768,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -794,7 +794,7 @@ create_customer! NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. @@ -802,7 +802,11 @@ The `association` method returns the associated object, if any. If no associated @customer = @order.customer ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@customer = @order.reload.customer +``` ##### `association=(associate)` @@ -1113,7 +1117,7 @@ The `has_one` association creates a one-to-one match with another model. In data When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -1139,7 +1143,7 @@ create_account! NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. @@ -1147,7 +1151,11 @@ The `association` method returns the associated object, if any. If no associated @account = @supplier.account ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@account = @supplier.reload.account +``` ##### `association=(associate)` @@ -1380,7 +1388,7 @@ The `has_many` association creates a one-to-many relationship with another model When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` @@ -1408,7 +1416,7 @@ end Each instance of the `Customer` model will have these methods: ```ruby -orders(force_reload = false) +orders orders<<(object, ...) orders.delete(object, ...) orders.destroy(object, ...) @@ -1426,7 +1434,7 @@ orders.create(attributes = {}) orders.create!(attributes = {}) ``` -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. @@ -1892,7 +1900,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` @@ -1920,7 +1928,7 @@ end Each instance of the `Part` model will have these methods: ```ruby -assemblies(force_reload = false) +assemblies assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) @@ -1945,7 +1953,7 @@ If the join table for a `has_and_belongs_to_many` association has additional col WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_many` association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a `has_many :through` association instead of `has_and_belongs_to_many`. -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index cd265331d6..e85f9fc9c6 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -412,7 +412,7 @@ Ruby version 2.2.2 (x86_64-linux) RubyGems version 2.4.6 Rack version 1.6 JavaScript Runtime Node.js (V8) -Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag +Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 8672525e2b..0dd99cf8e9 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -214,7 +214,6 @@ Every Rails application comes with a standard set of middleware which it uses in * `ActionDispatch::Cookies` sets cookies for the request. * `ActionDispatch::Session::CookieStore` is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the `config.action_controller.session_store` to an alternate value. Additionally, options passed to this can be configured by using `config.action_controller.session_options`. * `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value. -* `ActionDispatch::ParamsParser` parses out parameters from the request into `params`. * `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types. * `Rack::Head` converts HEAD requests to GET requests and serves them as so. @@ -1096,7 +1095,7 @@ you and wait for a connection from the pool. If it cannot get a connection, a timeout error similar to that given below will be thrown. ```ruby -ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: +ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds) ``` If you get the above error, you might want to increase the size of the diff --git a/guides/source/engines.md b/guides/source/engines.md index 3b1588b75a..6bbd68ff78 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -150,7 +150,7 @@ When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`: ```ruby -gem 'blorgh', path: "vendor/engines/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Don't forget to run `bundle install` as usual. By specifying it as a gem within @@ -639,7 +639,7 @@ However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`: ```ruby -gem 'blorgh', path: "/path/to/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Then run `bundle` to install the gem. diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 272a0e3623..ea79855919 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -51,7 +51,7 @@ Thus, the Ruby I18n gem is split into two parts: As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend. -NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. +NOTE: It is possible to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. ### The Public I18n API @@ -109,7 +109,7 @@ The **translations load path** (`I18n.load_path`) is an array of paths to files NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced. -The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. +The default `config/application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -117,7 +117,7 @@ The default `application.rb` file has instructions on how to add locales from an # config.i18n.default_locale = :de ``` -The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `application.rb`: +The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `config/application.rb`: ```ruby # config/initializers/locale.rb diff --git a/guides/source/kindle/layout.html.erb b/guides/source/kindle/layout.html.erb index f0a286210b..fd8746776b 100644 --- a/guides/source/kindle/layout.html.erb +++ b/guides/source/kindle/layout.html.erb @@ -14,12 +14,12 @@ <% if content_for? :header_section %> <%= yield :header_section %> - <div class="pagebreak"> + <div class="pagebreak"></div> <% end %> <% if content_for? :index_section %> <%= yield :index_section %> - <div class="pagebreak"> + <div class="pagebreak"></div> <% end %> <%= yield.html_safe %> diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb index 2c6d8e3bdf..5094fea4ca 100644 --- a/guides/source/kindle/toc.ncx.erb +++ b/guides/source/kindle/toc.ncx.erb @@ -32,12 +32,12 @@ </navPoint> <navPoint class="article" id="credits" playOrder="3"> <navLabel><text>Credits</text></navLabel> - <content src="credits.html"> + <content src="credits.html"/> </navPoint> <navPoint class="article" id="copyright" playOrder="4"> <navLabel><text>Copyright & License</text></navLabel> - <content src="copyright.html"> - </navPoint> + <content src="copyright.html"/> + </navPoint> </navPoint> <% play_order = 4 %> @@ -47,7 +47,7 @@ <text><%= section['name'] %></text> </navLabel> <content src="<%=section['documents'].first['url'] %>"/> - + <% section['documents'].each_with_index do |document, document_no| %> <navPoint class="article" id="_<%=section_no+1%>.<%=document_no+1%>" playOrder="<%=play_order +=1 %>"> <navLabel> diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index b7364536c3..edd54826cf 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -78,7 +78,7 @@ gem_group :development, :test do end ``` -### add_source(source, options = {}) +### add_source(source, options={}, &block) Adds the given source to the generated application's `Gemfile`. @@ -88,6 +88,14 @@ For example, if you need to source a gem from `"http://code.whytheluckystiff.net add_source "http://code.whytheluckystiff.net" ``` +If block is given, gem entries in block are wrapped into the source group. + +```ruby +add_source "http://gems.github.com/" do + gem "rspec-rails" +end +``` + ### environment/application(data=nil, options={}, &block) Adds a line inside the `Application` class for `config/application.rb`. diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 1e2fe94010..0db90fedb3 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -121,7 +121,6 @@ use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash -use ActionDispatch::ParamsParser use Rack::Head use Rack::ConditionalGet use Rack::ETag @@ -284,10 +283,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value. -**`ActionDispatch::ParamsParser`** - -* Parses out parameters from the request into `params`. - **`Rack::Head`** * Converts HEAD requests to `GET` requests and serves them as so. diff --git a/guides/source/routing.md b/guides/source/routing.md index e4799d93fa..1fd38c0940 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -7,7 +7,7 @@ This guide covers the user-facing features of Rails routing. After reading this guide, you will know: -* How to interpret the code in `routes.rb`. +* How to interpret the code in `config/routes.rb`. * How to construct your own routes, using either the preferred resourceful style or the `match` method. * What parameters to expect an action to receive. * How to automatically create paths and URLs using route helpers. @@ -1118,7 +1118,7 @@ Rails offers facilities for inspecting and testing your routes. To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output. -Both methods will list all of your routes, in the same order that they appear in `routes.rb`. For each route, you'll see: +Both methods will list all of your routes, in the same order that they appear in `config/routes.rb`. For each route, you'll see: * The route name (if any) * The HTTP verb used (if the route doesn't respond to all verbs) diff --git a/guides/source/security.md b/guides/source/security.md index 93c270064a..5a6ac9446a 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -245,7 +245,9 @@ Or the attacker places the code into the onmouseover event handler of an image: <img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." /> ``` -There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since `XMLHttpRequest` is subject to the browser Same-Origin policy - meaning only your site can initiate the request. +There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses. + +Note: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag. To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications: diff --git a/guides/source/testing.md b/guides/source/testing.md index aa3497fa13..435de30acc 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -302,7 +302,7 @@ specify to make your test failure messages clearer. It's not required. The above are a subset of assertions that minitest supports. For an exhaustive & more up-to-date list, please check [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically -[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) +[`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html). Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier. @@ -478,7 +478,7 @@ default. Loading involves three steps: 2. Load the fixture data into the table 3. Dump the fixture data into a method in case you want to access it directly -TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)) +TIP: In order to remove existing data from the database, Rails tries to disable referential integrity triggers (like foreign keys and check constraints). If you are getting annoying permission errors on running tests, make sure the database user has privilege to disable these triggers in testing environment. (In PostgreSQL, only superusers can disable all triggers. Read more about PostgreSQL permissions [here](http://blog.endpoint.com/2012/10/postgres-system-triggers-error.html)). #### Fixtures are Active Record objects @@ -750,9 +750,9 @@ end After a request has been made and processed, you will have 3 Hash objects ready for use: -* `cookies` - Any cookies that are set. -* `flash` - Any objects living in the flash. -* `session` - Any object living in session variables. +* `cookies` - Any cookies that are set +* `flash` - Any objects living in the flash +* `session` - Any object living in session variables As is the case with normal Hash objects, you can access the values by referencing the keys by string. You can also reference them by symbol name. For example: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 30c0fcb294..52464a1c51 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -314,11 +314,11 @@ Upgrading from Rails 4.0 to Rails 4.1 ### CSRF protection from remote `<script>` tags -Or, "whaaat my tests are failing!!!?" +Or, "whaaat my tests are failing!!!?" or "my `<script>` widget is busted!!" Cross-site request forgery (CSRF) protection now covers GET requests with -JavaScript responses, too. This prevents a third-party site from referencing -your JavaScript URL and attempting to run it to extract sensitive data. +JavaScript responses, too. This prevents a third-party site from remotely +referencing your JavaScript with a `<script>` tag to extract sensitive data. This means that your functional and integration tests that use @@ -334,8 +334,9 @@ xhr :get, :index, format: :js to explicitly test an `XmlHttpRequest`. -If you really mean to load JavaScript from remote `<script>` tags, skip CSRF -protection on that action. +Note: Your own `<script>` tags are treated as cross-origin and blocked by +default, too. If you really mean to load JavaScript from `<script>` tags, +you must now explicitly skip CSRF protection on those actions. ### Spring diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index faf6846e4b..f007d11c72 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,8 @@ +* Fix displaying mailer previews on non local requests when config + `action_mailer.show_previews` is set + + *Wojciech Wnętrzak* + * `rails server` will now honour the `PORT` environment variable *David Cornu* diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 88eade5c5a..21062f3a53 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -72,7 +72,6 @@ module Rails middleware.use ::ActionDispatch::Flash end - middleware.use ::ActionDispatch::ParamsParser middleware.use ::Rack::Head middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 38074e80e0..0f44f4694e 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -205,24 +205,21 @@ module Rails end def rails_gemfile_entry - if options.dev? - [ - GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), + dev_edge_common = [ GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), GemfileEntry.github('sprockets', 'rails/sprockets'), GemfileEntry.github('sass-rails', 'rails/sass-rails'), GemfileEntry.github('arel', 'rails/arel'), GemfileEntry.github('rack', 'rack/rack') ] + if options.dev? + [ + GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH) + ] + dev_edge_common elsif options.edge? [ - GemfileEntry.github('rails', 'rails/rails'), - GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), - GemfileEntry.github('sprockets', 'rails/sprockets'), - GemfileEntry.github('sass-rails', 'rails/sass-rails'), - GemfileEntry.github('arel', 'rails/arel'), - GemfileEntry.github('rack', 'rack/rack') - ] + GemfileEntry.github('rails', 'rails/rails') + ] + dev_edge_common else [GemfileEntry.version('rails', Rails::VERSION::STRING, diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 6a1c2faaab..975be07622 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -50,8 +50,6 @@ group :development do <% end -%> end <% end -%> -<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index f5b62e8fb3..5ca549a8c8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -7,7 +7,7 @@ # gem 'activerecord-jdbcmysql-adapter' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# http://dev.mysql.com/doc/refman/5.7/en/old-client.html # default: &default adapter: mysql diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index b0767bd93a..119c2fe2c3 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -7,7 +7,7 @@ # gem 'mysql2' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# http://dev.mysql.com/doc/refman/5.7/en/old-client.html # default: &default adapter: mysql2 diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index 2c3b04043f..f73e9a96ba 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -1,5 +1,5 @@ <% if namespaced? -%> -require_dependency "<%= namespaced_file_path %>/application_controller" +require_dependency "<%= namespaced_path %>/application_controller" <% end -%> <% module_namespacing do -%> diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 78a857e0f1..38004fe99a 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -3,7 +3,7 @@ require 'rails/application_controller' class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - before_action :require_local! + before_action :require_local!, unless: :show_previews? before_action :find_preview, only: :preview def index @@ -31,7 +31,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end else - @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) + @part = find_preferred_part(request.format, Mime::Type[:HTML], Mime::Type[:TEXT]) render action: 'email', layout: false, formats: %w[html] end else @@ -41,6 +41,10 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end protected + def show_previews? + ActionMailer::Base.show_previews + end + def find_preview candidates = [] params[:path].to_s.scan(%r{/|$}){ candidates << $` } diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index bb6e49966d..cf95cd8ad0 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -95,8 +95,8 @@ <% if @email.multipart? %> <dd> <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;"> - <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> - <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + <option <%= request.format == Mime::Type[:HTML] ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> + <option <%= request.format == Mime::Type[:TEXT] ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> </select> </dd> <% end %> diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index e462d2c15e..643d876a26 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -31,7 +31,7 @@ module ApplicationTests test "/rails/mailers is accessible with correct configuraiton" do add_to_config "config.action_mailer.show_previews = true" app("production") - get "/rails/mailers" + get "/rails/mailers", {}, {"REMOTE_ADDR" => "4.2.42.42"} assert_equal 200, last_response.status end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index d298e8d632..138c63266e 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -43,7 +43,6 @@ module ApplicationTests "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", - "ActionDispatch::ParamsParser", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag" @@ -70,7 +69,6 @@ module ApplicationTests "ActionDispatch::Callbacks", "ActiveRecord::ConnectionAdapters::ConnectionManagement", "ActiveRecord::QueryCache", - "ActionDispatch::ParamsParser", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag" diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index a040dd4cf6..0da0928b48 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -1,4 +1,3 @@ -# coding:utf-8 require "isolation/abstract_unit" require "active_support/core_ext/string/strip" diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index daf362357c..e5f10a89d3 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha DEFAULT_APP_FILES = %w( .gitignore @@ -117,35 +116,33 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [app_root] - Rails.application.config.root = app_moved_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + stub_rails_application(app_moved_root) do + Rails.application.stub(:is_a?, -> *args { Rails::Application }) do + FileUtils.mv(app_root, app_moved_root) - FileUtils.mv(app_root, app_moved_root) + # make sure we are in correct dir + FileUtils.cd(app_moved_root) - # make sure we are in correct dir - FileUtils.cd(app_moved_root) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: app_moved_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ - assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, + destination_root: app_moved_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ + assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ + end + end end def test_rails_update_generates_correct_session_key app_root = File.join(destination_root, 'myapp') run_generator [app_root] - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/ + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/ + end end def test_new_application_use_json_serialzier @@ -158,14 +155,12 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, 'myapp') run_generator [app_root] - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + end end def test_rails_update_does_not_create_callback_terminator_initializer @@ -174,14 +169,12 @@ class AppGeneratorTest < Rails::Generators::TestCase FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/initializers/callback_terminator.rb" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/callback_terminator.rb" + end end def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present @@ -190,14 +183,12 @@ class AppGeneratorTest < Rails::Generators::TestCase FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/callback_terminator.rb" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/callback_terminator.rb" + end end def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured @@ -206,14 +197,12 @@ class AppGeneratorTest < Rails::Generators::TestCase FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + end end def test_rails_update_does_not_create_active_record_belongs_to_required_by_default @@ -222,14 +211,12 @@ class AppGeneratorTest < Rails::Generators::TestCase FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end end def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present @@ -238,14 +225,12 @@ class AppGeneratorTest < Rails::Generators::TestCase FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + end end def test_application_names_are_not_singularized @@ -456,13 +441,15 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_usage_read_from_file - File.expects(:read).returns("USAGE FROM FILE") - assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + assert_called(File, :read, returns: "USAGE FROM FILE") do + assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + end end def test_default_usage - Rails::Generators::AppGenerator.expects(:usage_path).returns(nil) - assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + assert_called(Rails::Generators::AppGenerator, :usage_path, returns: nil) do + assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + end end def test_default_namespace @@ -538,18 +525,31 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring_binstubs jruby_skip "spring doesn't run on JRuby" - generator.stubs(:bundle_command).with('install') - generator.expects(:bundle_command).with('exec spring binstub --all').once - quietly { generator.invoke_all } + command_check = -> command do + @binstub_called ||= 0 + + case command + when 'install' + # Called when running bundle, we just want to stub it so nothing to do here. + when 'exec spring binstub --all' + @binstub_called += 1 + assert_equal 1, @binstub_called, "exec spring binstub --all expected to be called once, but was called #{@install_called} times." + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end end def test_spring_no_fork jruby_skip "spring doesn't run on JRuby" - Process.stubs(:respond_to?).with(:fork).returns(false) - run_generator + assert_called_with(Process, :respond_to?, [:fork], returns: false) do + run_generator - assert_file "Gemfile" do |content| - assert_no_match(/spring/, content) + assert_file "Gemfile" do |content| + assert_no_match(/spring/, content) + end end end @@ -651,18 +651,37 @@ class AppGeneratorTest < Rails::Generators::TestCase template = %{ after_bundle { run 'echo ran after_bundle' } } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + check_open = -> *args do + assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args + template + end - bundler_first = sequence('bundle, binstubs, after_bundle') - generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first) - generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first) - generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first) + sequence = ['install', 'exec spring binstub --all', 'echo ran after_bundle'] + ensure_bundler_first = -> command do + @sequence_step ||= 0 - quietly { generator.invoke_all } + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end + + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + quietly { generator.invoke_all } + end + end + end end protected + def stub_rails_application(root) + Rails.application.config.root = root + Rails.application.class.stub(:name, "Myapp") do + yield + end + end + def action(*args, &block) capture(:stdout) { generator.send(*args, &block) } end diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index e839b67960..c4ee6602c5 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -281,6 +281,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) end assert_file "test/controllers/test_app/admin/roles_controller_test.rb", diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index afccf9d885..0625e5fbd7 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/plugin/plugin_generator' require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha DEFAULT_PLUGIN_FILES = %w( .gitignore diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 77372fb514..acb78ec888 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -28,9 +28,22 @@ module SharedGeneratorTests def assert_generates_with_bundler(options = {}) generator([destination_root], options) - generator.expects(:bundle_command).with('install').once - generator.stubs(:bundle_command).with('exec spring binstub --all') - quietly { generator.invoke_all } + + command_check = -> command do + @install_called ||= 0 + + case command + when 'install' + @install_called += 1 + assert_equal 1, @install_called, "install expected to be called once, but was called #{@install_called} times" + when 'exec spring binstub --all' + # Called when running tests with spring, let through unscathed. + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end end def test_generation_runs_bundle_install @@ -91,8 +104,14 @@ module SharedGeneratorTests template = %{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + check_open = -> *args do + assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args + template + end + + generator([destination_root], template: path).stub(:open, check_open, template) do + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + end end def test_dev_option @@ -107,18 +126,19 @@ module SharedGeneratorTests end def test_skip_gemfile - generator([destination_root], skip_gemfile: true).expects(:bundle_command).never - quietly { generator.invoke_all } - assert_no_file 'Gemfile' + assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do + quietly { generator.invoke_all } + assert_no_file 'Gemfile' + end end def test_skip_bundle - generator([destination_root], skip_bundle: true).expects(:bundle_command).never - quietly { generator.invoke_all } - - # skip_bundle is only about running bundle install, ensure the Gemfile is still - # generated. - assert_file 'Gemfile' + assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do + quietly { generator.invoke_all } + # skip_bundle is only about running bundle install, ensure the Gemfile is still + # generated. + assert_file 'Gemfile' + end end def test_skip_git |