diff options
321 files changed, 3492 insertions, 1852 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' diff --git a/Gemfile.lock b/Gemfile.lock index da4614db2b..563a9f56be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,6 +89,14 @@ GIT sprockets (4.0.0) rack (> 1, < 3) +GIT + remote: git://github.com/rails/turbolinks.git + revision: 4bb563cd777875d3ad73cf007c26334f2aa8dc37 + branch: master + specs: + turbolinks (3.0.0) + coffee-rails + PATH remote: . specs: @@ -122,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) @@ -172,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) @@ -274,8 +281,6 @@ GEM thread_safe (0.3.5) timers (4.0.4) hitimes - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) tzinfo-data (1.2015.6) @@ -342,7 +347,7 @@ DEPENDENCIES sqlite3 (~> 1.3.6) stackprof sucker_punch - turbolinks + turbolinks! tzinfo-data uglifier (>= 1.3.0) w3c_validators @@ -34,7 +34,7 @@ Ruby code (ERB files). Views are typically rendered to generate a controller res or to generate the body of an email. In Rails, View generation is handled by Action View. You can read more about Action View in its [README](actionview/README.rdoc). -Active Record, Action Pack, and Action View can each be used independently outside Rails. +Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails. In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library to generate and send emails; Active Job ([README](activejob/README.md)), a framework for declaring jobs and making them run on a variety of queueing 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 73a833a23b..bb15edee63 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,21 @@ +* 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* 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 6c0a072b73..78b43f2fbe 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -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( diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 0384740fef..beeaae9d0c 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -135,7 +135,7 @@ 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 @@ -187,6 +187,7 @@ module ActionController set_request!(request) set_response!(response) process(name) + request.commit_flash to_a end 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 667c7f87ca..7db8d13e24 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -213,29 +213,6 @@ module ActionController end class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) # :nodoc: - def initialize(response, header) - @response = response - super(header) - end - - def []=(k,v) - if @response.committed? - raise ActionDispatch::IllegalStateError, 'header already sent' - end - - super - end - - def merge(other) - self.class.new @response, __getobj__.merge(other) - end - - def to_hash - __getobj__.dup - end - end - private def before_committed @@ -256,14 +233,6 @@ module ActionController body.each { |part| buf.write part } buf end - - def merge_default_headers(original, default) - Header.new self, super - end - - def handle_conditional_get! - super unless committed? - end end def process(name) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 88a4818c16..fc42fe5c07 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -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 00b551af94..1ecccf9864 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -64,7 +64,7 @@ module ActionController end def _set_html_content_type - self.content_type = Mime::HTML.to_s + self.content_type = Mime::Type[:HTML].to_s end def _set_rendered_content_type(format) 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/test_case.rb b/actionpack/lib/action_controller/test_case.rb index fbbaa1a887..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,26 +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.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 + @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } data = non_path_parameters.to_query - self.request_parameters = non_path_parameters end end @@ -136,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 @@ -401,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' @@ -504,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 @@ -584,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..36e90e5855 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 @@ -91,7 +131,7 @@ module Mime exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list delete_at(text_xml_idx) # delete text_xml from the list elsif text_xml_idx - text_xml.name = Mime::XML.to_s + text_xml.name = Mime::Type[:XML].to_s end # Look for more specific XML-based types and sort them ahead of app/xml @@ -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) @@ -200,11 +255,11 @@ module Mime parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP end - # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, - # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. + # For an input of <tt>'text'</tt>, returns <tt>[Mime::Type[:JSON], Mime::Type[:XML], Mime::Type[:ICS], + # Mime::Type[:HTML], Mime::Type[:CSS], Mime::Type[:CSV], Mime::Type[:JS], Mime::Type[:YAML], Mime::Type[:TEXT]</tt>. # - # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS, - # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>. + # For an input of <tt>'application'</tt>, returns <tt>[Mime::Type[:HTML], Mime::Type[:JS], + # Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:ATOM], Mime::Type[:JSON], Mime::Type[:RSS], Mime::Type[:URL_ENCODED_FORM]</tt>. def parse_data_with_trailing_star(input) Mime::SET.select { |m| m =~ input } end @@ -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..04828f7c87 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -32,5 +32,5 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr 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, []) +# Create Mime::Type[:ALL] but do not add it to the SET. +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 b2566c4820..bf20a33d36 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -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 @@ -378,6 +384,9 @@ module ActionDispatch get_header("action_dispatch.logger".freeze) end + def commit_flash + end + private def check_method(name) HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}") diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 45ffacd6f5..85d9c3be00 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,6 +32,29 @@ module ActionDispatch # :nodoc: # end # end class Response + class Header < DelegateClass(Hash) # :nodoc: + def initialize(response, header) + @response = response + super(header) + end + + def []=(k,v) + if @response.committed? + raise ActionDispatch::IllegalStateError, 'header already sent' + end + + super + end + + def merge(other) + self.class.new @response, __getobj__.merge(other) + end + + def to_hash + __getobj__.dup + end + end + # The request that the response is responding to. attr_accessor :request @@ -103,14 +126,22 @@ 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 + @header = Header.new(self, header) self.body, self.status = body, status @@ -340,15 +371,12 @@ module ActionDispatch # :nodoc: return if committed? assign_default_content_type_and_charset! handle_conditional_get! + handle_no_content! end 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 @@ -361,7 +389,7 @@ module ActionDispatch # :nodoc: return if content_type ct = parse_content_type - set_content_type(ct.mime_type || Mime::HTML.to_s, + set_content_type(ct.mime_type || Mime::Type[:HTML].to_s, ct.charset || self.class.default_charset) end @@ -401,10 +429,15 @@ module ActionDispatch # :nodoc: end end + def handle_no_content! + if NO_CONTENT_CODES.include?(@status) + @header.delete CONTENT_TYPE + @header.delete 'Content-Length' + end + end + def rack_response(status, header) if NO_CONTENT_CODES.include?(status) - header.delete CONTENT_TYPE - header.delete 'Content-Length' [status, header, []] else [status, header, RackBody.new(self)] 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 b653e4eacd..7ed77352ae 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -396,17 +396,30 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } - @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } + headers[HTTP_HEADER] = make_set_cookie_header headers[HTTP_HEADER] end mattr_accessor :always_write_cookie self.always_write_cookie = false private - def write_cookie?(cookie) - request.ssl? || !cookie[:secure] || always_write_cookie - end + + def make_set_cookie_header(header) + header = @set_cookies.inject(header) { |m, (k, v)| + if write_cookie?(v) + ::Rack::Utils.add_cookie_to_header(m, k, v) + else + m + end + } + @delete_cookies.inject(header) { |m, (k, v)| + ::Rack::Utils.add_remove_cookie_to_header(m, k, v) + } + end + + def write_cookie?(cookie) + request.ssl? || !cookie[:secure] || always_write_cookie + end end class AbstractCookieJar # :nodoc: 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..c51dcd542a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,25 +1,6 @@ require 'active_support/core_ext/hash/keys' module ActionDispatch - class Request - # Access the contents of the flash. Use <tt>flash["notice"]</tt> to - # read a notice you put there or <tt>flash["notice"] = "hello"</tt> - # to put a new one. - def flash - flash = flash_hash - return flash if flash - self.flash = Flash::FlashHash.from_session_value(session["flash"]) - end - - def flash=(flash) - set_header Flash::KEY, flash - end - - def flash_hash # :nodoc: - get_header Flash::KEY - end - end - # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can @@ -57,6 +38,40 @@ module ActionDispatch class Flash KEY = 'action_dispatch.request.flash_hash'.freeze + module RequestMethods + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to + # read a notice you put there or <tt>flash["notice"] = "hello"</tt> + # to put a new one. + def flash + flash = flash_hash + return flash if flash + self.flash = Flash::FlashHash.from_session_value(session["flash"]) + end + + def flash=(flash) + set_header Flash::KEY, flash + end + + def flash_hash # :nodoc: + get_header Flash::KEY + end + + def commit_flash # :nodoc: + session = self.session || {} + flash_hash = self.flash_hash + + if flash_hash && (flash_hash.present? || session.key?('flash')) + session["flash"] = flash_hash.to_session_value + self.flash = flash_hash.dup + end + + if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) + session.key?('flash') && session['flash'].nil? + session.delete('flash') + end + end + end + class FlashNow #:nodoc: attr_accessor :flash @@ -268,26 +283,10 @@ module ActionDispatch end end - def initialize(app) - @app = app - end - - def call(env) - req = ActionDispatch::Request.new env - @app.call(env) - ensure - session = Request::Session.find(req) || {} - flash_hash = req.flash_hash - - if flash_hash && (flash_hash.present? || session.key?('flash')) - session["flash"] = flash_hash.to_session_value - req.flash = flash_hash.dup - end + def self.new(app) app; end + end - if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?) - session.key?('flash') && session['flash'].nil? - session.delete('flash') - end - end + class Request + prepend Flash::RequestMethods end end 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 02b6cfe727..0e636b8257 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -62,11 +62,7 @@ module ActionDispatch # would set the session cookie to expire automatically 14 days after creation. # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and # <tt>:httponly</tt>. - class CookieStore < Rack::Session::Abstract::Persisted - include Compatibility - include StaleSessionCheck - include SessionObject - + class CookieStore < AbstractStore def initialize(app, options={}) super(app, options.merge!(:cookie_only => true)) end 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/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/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 06bf9dec74..40c97abd35 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -974,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 @@ -999,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/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..68083ed747 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::Type.registered?(: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,60 +142,71 @@ 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 - # Don't test Mime::ALL, since it Mime::ALL#html? == true + # Don't test Mime::Type[:ALL], since it Mime::Type[:ALL].html? == true 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) } - + # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE] + 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 "deprecated const_defined?" do + assert_deprecated { Mime.const_defined?(:ALL) } 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) } + # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE] + 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 5d74424de7..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 @@ -149,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) @@ -178,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 @@ -210,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 @@ -229,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 @@ -317,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 } @@ -326,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 @@ -342,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..15d51e5d6c 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 @@ -498,17 +497,6 @@ module ActionDispatch private - def add_routes router, paths, anchor = true - paths.each do |path| - if String === path - path = Path::Pattern.from_string path - else - path - end - add_route @app, path, {}, [], {}, {} - end - end - def rails_env env, klass = ActionDispatch::Request klass.new(rack_env(env)) end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 9ea250643a..82a88952c3 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error + when the only input on the form is the `collection_radio_buttons`. + + *Mauro George* + * `url_for` does not modify its arguments when generating polymorphic URLs. *Bernerd Schaefer* 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 ebf5ea5b77..2a367b85af 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]" /> @@ -1038,7 +1038,7 @@ module ActionView # date_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="date" /> # - # The default value is generated by trying to call "to_date" + # The default value is generated by trying to call +strftime+ with "%Y-%m-%d" # on the object's value, which makes it behave as expected for instances # of DateTime and ActiveSupport::TimeWithZone. You can still override that # by passing the "value" option explicitly, e.g. @@ -1617,7 +1617,14 @@ module ActionView @auto_index end - record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]" + record_name = if index + "#{object_name}[#{index}][#{record_name}]" + elsif record_name.to_s.end_with?('[]') + record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") + "#{object_name}#{record_name}" + else + "#{object_name}[#{record_name}]" + end fields_options[:child_index] = index @template.fields_for(record_name, record_object, fields_options, &block) diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 728e633bbf..430051379d 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -548,7 +548,7 @@ module ActionView end # Returns a string of option tags for pretty much any time zone in the - # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it + # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it # marked as the selected option tag. You can also supply an array of # ActiveSupport::TimeZone objects as +priority_zones+, so that they will # be listed above the rest of the (long) list. (You can use @@ -556,7 +556,7 @@ module ActionView # of the US time zones, or a Regexp to select the zones of your choice) # # The +selected+ parameter must be either +nil+, or a string that names - # a ActiveSupport::TimeZone. + # an ActiveSupport::TimeZone. # # By default, +model+ is the ActiveSupport::TimeZone constant (which can # be obtained in Active Record as a value object). The only requirement @@ -644,6 +644,24 @@ module ActionView # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| # b.label(:"data-value" => b.value) { b.radio_button + b.text } # end + # + # ==== Gotcha + # + # The HTML specification says when nothing is select on a collection of radio buttons + # web browsers do not send any value to server. + # Unfortunately this introduces a gotcha: + # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So, + # any strong parameters idiom like + # + # params.require(:user).permit(...) + # + # will raise an error since no +{user: ...}+ will be present. + # + # To prevent this the helper generates an auxiliary hidden field before + # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value. + # + # In case if you don't want the helper to generate this hidden field you can specify + # <tt>include_hidden: false</tt> option. def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) end 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/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 1765fa6558..3256d44e18 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -9,29 +9,13 @@ module ActionView class CheckBoxBuilder < Builder # :nodoc: def check_box(extra_html_options={}) html_options = extra_html_options.merge(@input_html_options) + html_options[:multiple] = true @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end def render(&block) - rendered_collection = render_collection do |item, value, text, default_html_options| - default_html_options[:multiple] = true - builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end - - # Append a hidden field to make sure something will be sent back to the - # server if all check boxes are unchecked. - if @options.fetch(:include_hidden, true) - rendered_collection + hidden_field - else - rendered_collection - end + render_collection_for(CheckBoxBuilder, &block) end private @@ -39,11 +23,6 @@ module ActionView def render_component(builder) builder.check_box + builder.label end - - def hidden_field - hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" - @template_object.hidden_field_tag(hidden_name, "", id: nil) - end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 8050638363..fea4c8d4ec 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -79,6 +79,32 @@ module ActionView yield item, value, text, default_html_options.merge(additional_html_options) end.join.html_safe end + + def render_collection_for(builder_class, &block) #:nodoc: + options = @options.stringify_keys + rendered_collection = render_collection do |item, value, text, default_html_options| + builder = instantiate_builder(builder_class, item, value, text, default_html_options) + + if block_given? + @template_object.capture(builder, &block) + else + render_component(builder) + end + end + + # Append a hidden field to make sure something will be sent back to the + # server if all radio buttons are unchecked. + if options.fetch('include_hidden', true) + rendered_collection + hidden_field + else + rendered_collection + end + end + + def hidden_field #:nodoc: + hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" + @template_object.hidden_field_tag(hidden_name, "", id: nil) + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index 20be34c1f2..21aaf122f8 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -14,15 +14,7 @@ module ActionView end def render(&block) - render_collection do |item, value, text, default_html_options| - builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end + render_collection_for(RadioButtonBuilder, &block) end private 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/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/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_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index b193d387c3..41932d15ee 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -198,6 +198,41 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'input[type=radio][value=false][checked=checked]' end + test 'collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name + + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1 + end + + test 'collection radio buttons generates a hidden field using the given :name in :html_options' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" } + + assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1 + end + + test 'collection radio buttons generates a hidden field with index if it was provided' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 } + + assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1 + end + + test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false + + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + end + + test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false + + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + end + # COLLECTION CHECK BOXES test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] @@ -235,6 +270,13 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0 end + test 'collection check boxes does not generate a hidden field if include_hidden option is false with key as string' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_check_boxes :user, :category_ids, collection, :id, :name, 'include_hidden' => false + + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + end + test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index aef137935a..41f31f1582 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>}, @@ -1596,7 +1596,8 @@ class FormHelperTest < ActionView::TestCase "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='post_active_true'>true</label>" + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "<label for='post_active_false'>false</label>" + "<label for='post_active_false'>false</label>" + + "<input type='hidden' name='post[active][]' value='' />" end assert_dom_equal expected, output_buffer @@ -1619,7 +1620,8 @@ class FormHelperTest < ActionView::TestCase "true</label>" + "<label for='post_active_false'>"+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "false</label>" + "false</label>" + + "<input type='hidden' name='post[active][]' value='' />" end assert_dom_equal expected, output_buffer @@ -1645,6 +1647,7 @@ class FormHelperTest < ActionView::TestCase "<label for='post_active_false'>"+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + "false</label>"+ + "<input type='hidden' name='post[active][]' value='' />" + "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1663,7 +1666,8 @@ class FormHelperTest < ActionView::TestCase "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='foo_post_active_true'>true</label>" + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + - "<label for='foo_post_active_false'>false</label>" + "<label for='foo_post_active_false'>false</label>" + + "<input type='hidden' name='post[active][]' value='' />" end assert_dom_equal expected, output_buffer @@ -1681,7 +1685,8 @@ class FormHelperTest < ActionView::TestCase "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + "<label for='post_1_active_true'>true</label>" + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + - "<label for='post_1_active_false'>false</label>" + "<label for='post_1_active_false'>false</label>" + + "<input type='hidden' name='post[1][active][]' value='' />" end assert_dom_equal expected, output_buffer @@ -2292,6 +2297,27 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_deep_nested_fields_for + @comment.save + form_for(:posts) do |f| + f.fields_for('post[]', @post) do |f2| + f2.text_field(:id) + @post.comments.each do |comment| + concat f2.fields_for('comment[]', comment) { |c| + concat c.text_field(:name) + } + end + end + end + + expected = whole_form do + "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />" + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_nested_collections form_for(@post, as: 'post[]') do |f| concat f.text_field(:title) diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 7e1324c011..8db35d02e1 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -450,21 +450,21 @@ class FormTagHelperTest < ActionView::TestCase ActionView::Base.automatically_disable_submit_tag = true end - def test_data_disable_with_string + def test_submit_tag_having_data_disable_with_string assert_dom_equal( %(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" }) ) end - def test_data_disable_with_boolean + def test_submit_tag_having_data_disable_with_boolean assert_dom_equal( %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" }) ) end - def test_data_hash_disable_with_boolean + def test_submit_tag_having_data_hash_disable_with_boolean assert_dom_equal( %(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />), submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } }) 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/render_test.rb b/actionview/test/template/render_test.rb index 9c2c9507b7..00fc28a522 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -627,6 +627,16 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) end + test "with caching with custom key and rendering with different key" do + customer = Customer.new("david") + key = cache_key([customer, 'key'], "test/_customer") + + ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + + assert_equal "Hello: david", + @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] }) + end + test "automatic caching with inferred cache name" do customer = CachedCustomer.new("david") key = cache_key(customer, "test/_cached_customer") 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/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 965b556fab..b5742191eb 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add job priorities to Active Job. + + *wvengen* + * Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that queue jobs to a `concurrent-ruby` thread pool. 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/base.rb b/activejob/lib/active_job/base.rb index 5d7c4cfb91..e5f09f65fb 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -1,6 +1,7 @@ require 'active_job/core' require 'active_job/queue_adapter' require 'active_job/queue_name' +require 'active_job/queue_priority' require 'active_job/enqueuing' require 'active_job/execution' require 'active_job/callbacks' @@ -57,6 +58,7 @@ module ActiveJob #:nodoc: include Core include QueueAdapter include QueueName + include QueuePriority include Enqueuing include Execution include Callbacks diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index eac7279309..19b900a285 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -18,6 +18,9 @@ module ActiveJob # Queue in which the job will reside. attr_writer :queue_name + # Priority that the job will have (lower is more priority). + attr_writer :priority + # ID optionally provided by adapter attr_accessor :provider_job_id @@ -43,6 +46,7 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # @@ -51,6 +55,7 @@ module ActiveJob # VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last) # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last) # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last) def set(options={}) ConfiguredJob.new(self, options) end @@ -62,6 +67,7 @@ module ActiveJob @arguments = arguments @job_id = SecureRandom.uuid @queue_name = self.class.queue_name + @priority = self.class.priority end # Returns a hash with the job data that can safely be passed to the @@ -71,6 +77,7 @@ module ActiveJob 'job_class' => self.class.name, 'job_id' => job_id, 'queue_name' => queue_name, + 'priority' => priority, 'arguments' => serialize_arguments(arguments), 'locale' => I18n.locale } @@ -99,6 +106,7 @@ module ActiveJob def deserialize(job_data) self.job_id = job_data['job_id'] self.queue_name = job_data['queue_name'] + self.priority = job_data['priority'] self.serialized_arguments = job_data['arguments'] self.locale = job_data['locale'] || I18n.locale end diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 98d92385dd..22154457fd 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -32,6 +32,7 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # @@ -54,6 +55,7 @@ module ActiveJob # * <tt>:wait</tt> - Enqueues the job with the specified delay # * <tt>:wait_until</tt> - Enqueues the job at the time specified # * <tt>:queue</tt> - Enqueues the job on the specified queue + # * <tt>:priority</tt> - Enqueues the job with the specified priority # # ==== Examples # @@ -61,10 +63,12 @@ module ActiveJob # my_job_instance.enqueue wait: 5.minutes # my_job_instance.enqueue queue: :important # my_job_instance.enqueue wait_until: Date.tomorrow.midnight + # my_job_instance.enqueue priority: 10 def enqueue(options={}) self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait] self.scheduled_at = options[:wait_until].to_f if options[:wait_until] self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue] + self.priority = options[:priority].to_i if options[:priority] run_callbacks :enqueue do if self.scheduled_at self.class.queue_adapter.enqueue_at self, self.scheduled_at diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb index ac83da2b9c..0a785fad3b 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -14,13 +14,13 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter def enqueue(job) #:nodoc: - delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority) job.provider_job_id = delayed_job.id delayed_job end def enqueue_at(job, timestamp) #:nodoc: - delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority, run_at: Time.at(timestamp)) job.provider_job_id = delayed_job.id delayed_job end diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index 90947aa98d..ab13689747 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -16,13 +16,13 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :que class QueAdapter def enqueue(job) #:nodoc: - que_job = JobWrapper.enqueue job.serialize + que_job = JobWrapper.enqueue job.serialize, priority: job.priority job.provider_job_id = que_job.attrs["job_id"] que_job end def enqueue_at(job, timestamp) #:nodoc: - que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp) + que_job = JobWrapper.enqueue job.serialize, priority: job.priority, run_at: Time.at(timestamp) job.provider_job_id = que_job.attrs["job_id"] que_job end diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb new file mode 100644 index 0000000000..01d84910ff --- /dev/null +++ b/activejob/lib/active_job/queue_priority.rb @@ -0,0 +1,44 @@ +module ActiveJob + module QueuePriority + extend ActiveSupport::Concern + + # Includes the ability to override the default queue priority. + module ClassMethods + mattr_accessor(:default_priority) + + # Specifies the priority of the queue to create the job with. + # + # class PublishToFeedJob < ActiveJob::Base + # queue_with_priority 50 + # + # def perform(post) + # post.to_feed! + # end + # end + # + # Specify either an argument or a block. + def queue_with_priority(priority=nil, &block) + if block_given? + self.priority = block + else + self.priority = priority + end + end + end + + included do + class_attribute :priority, instance_accessor: false + + self.priority = default_priority + end + + # Returns the priority that the job will be created with + def priority + if @priority.is_a?(Proc) + @priority = instance_exec(&@priority) + end + @priority + end + + end +end diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb new file mode 100644 index 0000000000..ca17b51dad --- /dev/null +++ b/activejob/test/cases/queue_priority_test.rb @@ -0,0 +1,47 @@ +require 'helper' +require 'jobs/hello_job' + +class QueuePriorityTest < ActiveSupport::TestCase + test 'priority unset by default' do + assert_equal nil, HelloJob.priority + end + + test 'uses given priority' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority 90 + assert_equal 90, HelloJob.new.priority + ensure + HelloJob.priority = original_priority + end + end + + test 'evals block given to priority to determine priority' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority { 25 } + assert_equal 25, HelloJob.new.priority + ensure + HelloJob.priority = original_priority + end + end + + test 'can use arguments to determine priority in priority block' do + original_priority = HelloJob.priority + + begin + HelloJob.queue_with_priority { self.arguments.first=='1' ? 99 : 11 } + assert_equal 99, HelloJob.new('1').priority + assert_equal 11, HelloJob.new('3').priority + ensure + HelloJob.priority = original_priority + end + end + + test 'uses priority passed to #set' do + job = HelloJob.set(priority: 123).perform_later + assert_equal 123, job.priority + end +end 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/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 125ba54302..e435ed4aa6 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -84,4 +84,16 @@ class QueuingTest < ActiveSupport::TestCase I18n.locale = :en end end + + test 'should run job with higher priority first' do + skip unless adapter_is?(:delayed_job, :que) + + wait_until = Time.now + 3.seconds + TestJob.set(wait_until: wait_until, priority: 20).perform_later "#{@id}.1" + TestJob.set(wait_until: wait_until, priority: 10).perform_later "#{@id}.2" + wait_for_jobs_to_finish_for(10.seconds) + assert job_executed "#{@id}.1" + assert job_executed "#{@id}.2" + assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1") + end 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/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index 39aee6d407..8319d09520 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -42,8 +42,12 @@ module TestCaseHelpers end end - def job_executed - Dummy::Application.root.join("tmp/#{@id}").exist? + def job_executed(id=@id) + Dummy::Application.root.join("tmp/#{id}").exist? + end + + def job_executed_at(id=@id) + File.new(Dummy::Application.root.join("tmp/#{id}")).ctime end def job_output diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb index 0232da1370..0950e52d28 100644 --- a/activejob/test/support/que/inline.rb +++ b/activejob/test/support/que/inline.rb @@ -6,6 +6,7 @@ Que::Job.class_eval do if args.last.is_a?(Hash) options = args.pop options.delete(:run_at) + options.delete(:priority) args << options unless options.empty? end self.run(*args) 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/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0169c20e0b..0ab8df42f5 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -203,7 +203,7 @@ module ActiveModel # Returns +true+ if attr_name were changed before the model was saved, # +false+ otherwise. def previous_changes_include?(attr_name) - @previously_changed.include?(attr_name) + previous_changes.include?(attr_name) end # Removes current changes and makes them accessible through +previous_changes+. @@ -225,7 +225,7 @@ module ActiveModel # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - @previously_changed[attr] if attribute_previously_changed?(attr) + previous_changes[attr] if attribute_previously_changed?(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. 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/naming.rb b/activemodel/lib/active_model/naming.rb index 213f2d5b6a..de5fb27467 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -164,7 +164,7 @@ module ActiveModel @route_key << "_index" if @plural == @singular end - # Transform the model name into a more humane format, using I18n. By default, + # Transform the model name into a more human format, using I18n. By default, # it will underscore then humanize the class name. # # class BlogPost 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..facea12704 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: @@ -11,16 +11,21 @@ module ActiveRecord end end + define_method(:assert_valid_value) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + define_method(:value_from_multiparameter_assignment) do |values_hash| defaults.each do |k, v| values_hash[k] ||= v 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..5fea0561a6 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 @@ -91,6 +91,9 @@ module ActiveRecord limit == other.limit end + def assert_valid_value(*) + end + private # Convenience method for types which do not need separate type casting diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 1bcfedb35d..c5c0cd4636 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,16 +14,63 @@ module ActiveModel end private + def setup!(klass) - attr_readers = attributes.reject { |name| klass.attribute_method?(name) } - attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } - klass.send(:attr_reader, *attr_readers) - klass.send(:attr_writer, *attr_writers) + klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes))) end def acceptable_option?(value) Array(options[:accept]).include?(value) end + + class LazilyDefineAttributes < Module + def initialize(attribute_definition) + define_method(:respond_to_missing?) do |method_name, include_private=false| + super(method_name, include_private) || attribute_definition.matches?(method_name) + end + + define_method(:method_missing) do |method_name, *args, &block| + if attribute_definition.matches?(method_name) + attribute_definition.define_on(self.class) + send(method_name, *args, &block) + else + super(method_name, *args, &block) + end + end + end + end + + class AttributeDefinition + def initialize(attributes) + @attributes = attributes.map(&:to_s) + end + + def matches?(method_name) + attr_name = convert_to_reader_name(method_name) + attributes.include?(attr_name) + end + + def define_on(klass) + attr_readers = attributes.reject { |name| klass.attribute_method?(name) } + attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } + klass.send(:attr_reader, *attr_readers) + klass.send(:attr_writer, *attr_writers) + end + + protected + + attr_reader :attributes + + private + + def convert_to_reader_name(method_name) + attr_name = method_name.to_s + if attr_name.end_with?("=") + attr_name = attr_name[0..-2] + end + attr_name + end + end end module HelperMethods 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/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 918097449b..6c0722e43c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,92 @@ +* Don't require a database connection to load a class which uses acceptance + validations. + + *Sean Griffin* + +* 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* + +* Add `ActiveRecord::Base.ignored_columns` to make some columns + invisible from ActiveRecord. + + *Jean Boussier* + +* `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. 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/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 2b7e4f28c5..021bc32237 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions' module ActiveRecord module Associations - # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and - # ActiveRecord::Associations::ThroughAssociationScope + # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: attr_reader :aliases diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 79b6b80d84..9994b72158 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -127,7 +127,7 @@ module ActiveRecord # # ] # # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> - # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4 # # person.pets.find(2) { |pet| pet.name.downcase! } # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> @@ -443,7 +443,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # # If it is set to <tt>:delete_all</tt>, all the objects are deleted # *without* calling their +destroy+ method. @@ -463,7 +463,7 @@ module ActiveRecord # person.pets.delete_all # # Pet.find(1, 2, 3) - # # => ActiveRecord::RecordNotFound + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) def delete_all(dependent = nil) @association.delete_all(dependent) end @@ -557,7 +557,7 @@ module ActiveRecord # # => [#<Pet id: 2, name: "Spook", person_id: 1>] # # Pet.find(1, 3) - # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3) # # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted # *without* calling their +destroy+ method. @@ -585,7 +585,7 @@ module ActiveRecord # # ] # # Pet.find(1) - # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1 # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and executes delete on them. @@ -649,7 +649,7 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3) # # You can pass +Fixnum+ or +String+ values, it finds the records # responding to the +id+ and then deletes them from the database. @@ -681,7 +681,7 @@ module ActiveRecord # person.pets.size # => 0 # person.pets # => [] # - # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6) def destroy(*records) @association.destroy(*records) end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 38bda0d2a5..7da20d8eea 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -103,7 +103,7 @@ module ActiveRecord counter = reflection.counter_cache_column owner[counter] ||= 0 owner[counter] += difference - owner.send(:clear_attribute_changes, counter) # eww + owner.send(:clear_attribute_change, counter) # eww end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index b1e05c32b5..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 diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 1dc8bff193..92792a7a15 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -154,7 +154,7 @@ module ActiveRecord scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end - scope.unscope_values = Array(values[:unscope]) + scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) klass.default_scoped.merge(scope) end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 73dd3fa041..74b44810d4 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -34,14 +34,10 @@ module ActiveRecord def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = original_value unless defined?(@value) + @value = type_cast(value_before_type_cast) unless defined?(@value) @value end - def original_value - type_cast(value_before_type_cast) - end - def value_for_database type.serialize(value) end @@ -55,6 +51,7 @@ module ActiveRecord end def with_value_from_user(value) + type.assert_valid_value(value) self.class.from_user(name, value, type) end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index e0bee8c17e..501590cf0e 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -16,7 +16,7 @@ module ActiveRecord end end - def changed_in_place_from?(old_value) + def changed_from?(old_value) super || changed_from?(database_default.value) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 0862306749..ca6ba18fe0 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 @@ -106,7 +106,7 @@ module ActiveRecord # end # # Person.instance_method_already_implemented?(:save) - # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord + # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # # Person.instance_method_already_implemented?(:name) # # => false diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 0171ef3bdf..43c15841c2 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'active_record/attribute_mutation_tracker' module ActiveRecord module AttributeMethods @@ -38,19 +39,40 @@ module ActiveRecord end end + def init_internals + super + @mutation_tracker = AttributeMutationTracker.new(@attributes, @attributes.dup) + end + def initialize_dup(other) # :nodoc: super - calculate_changes_from_defaults + @mutation_tracker = AttributeMutationTracker.new(@attributes, + self.class._default_attributes.deep_dup) end def changes_applied - super - store_original_raw_attributes + @previous_mutation_tracker = @mutation_tracker + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes end def clear_changes_information + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes + end + + def raw_write_attribute(attr_name, *) + result = super + clear_attribute_change(attr_name) + result + end + + def clear_attribute_changes(attr_names) super - original_raw_attributes.clear + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end end def changed_attributes @@ -59,7 +81,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(attributes_changed_in_place).freeze + super.reverse_merge(@mutation_tracker.changed_values).freeze end end @@ -69,59 +91,22 @@ module ActiveRecord end end + def previous_changes + previous_mutation_tracker.changes + end + def attribute_changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) + @mutation_tracker.changed_in_place?(attr_name) end private def changes_include?(attr_name) - super || attribute_changed_in_place?(attr_name) - end - - def calculate_changes_from_defaults - @changed_attributes = nil - self.class.column_defaults.each do |attr, orig_value| - set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) - end - end - - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s - - old_value = old_attribute_value(attr) - - result = super - store_original_raw_attribute(attr) - save_changed_attribute(attr, old_value) - result - end - - def raw_write_attribute(attr, value) - attr = attr.to_s - - result = super - original_raw_attributes[attr] = value - result - end - - def save_changed_attribute(attr, old_value) - clear_changed_attributes_cache - if attribute_changed_by_setter?(attr) - clear_attribute_changes(attr) unless _field_changed?(attr, old_value) - else - set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) - end + super || @mutation_tracker.changed?(attr_name) end - def old_attribute_value(attr) - if attribute_changed?(attr) - changed_attributes[attr] - else - clone_attribute_value(:_read_attribute, attr) - end + def clear_attribute_change(attr_name) + @mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -136,41 +121,12 @@ module ActiveRecord changed & self.class.column_names end - def _field_changed?(attr, old_value) - @attributes[attr].changed_from?(old_value) - end - - def attributes_changed_in_place - changed_in_place.each_with_object({}) do |attr_name, h| - orig = @attributes[attr_name].original_value - h[attr_name] = orig - end - end - - def changed_in_place - self.class.attribute_names.select do |attr_name| - attribute_changed_in_place?(attr_name) - end - end - - def original_raw_attribute(attr_name) - original_raw_attributes.fetch(attr_name) do - read_attribute_before_type_cast(attr_name) - end - end - - def original_raw_attributes - @original_raw_attributes ||= {} + def store_original_attributes + @mutation_tracker = @mutation_tracker.now_tracking(@attributes) end - def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil - end - - def store_original_raw_attributes - attribute_names.each do |attr| - store_original_raw_attribute(attr) - end + def previous_mutation_tracker + @previous_mutation_tracker ||= NullMutationTracker.new end def cache_changed_attributes 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/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index f9beb43e4b..9e693b6aee 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -13,7 +13,7 @@ module ActiveRecord set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - user_input_in_time_zone(value) || super + super(user_input_in_time_zone(value)) || super rescue ArgumentError nil end 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_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb new file mode 100644 index 0000000000..08eb8ba7ac --- /dev/null +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -0,0 +1,84 @@ +module ActiveRecord + class AttributeMutationTracker # :nodoc: + def initialize(attributes, original_attributes) + @attributes = attributes + @original_attributes = original_attributes + end + + def changed_values + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = original_attributes.fetch_value(attr_name) + end + end + end + + def changes + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = [original_attributes.fetch_value(attr_name), attributes.fetch_value(attr_name)] + end + end + end + + def changed?(attr_name) + attr_name = attr_name.to_s + modified?(attr_name) || changed_in_place?(attr_name) + end + + def changed_in_place?(attr_name) + original_database_value = original_attributes[attr_name].value_before_type_cast + attributes[attr_name].changed_in_place_from?(original_database_value) + end + + def forget_change(attr_name) + attr_name = attr_name.to_s + original_attributes[attr_name] = attributes[attr_name].dup + end + + def now_tracking(new_attributes) + AttributeMutationTracker.new(new_attributes, clean_copy_of(new_attributes)) + end + + protected + + attr_reader :attributes, :original_attributes + + private + + def attr_names + attributes.keys + end + + def modified?(attr_name) + attributes[attr_name].changed_from?(original_attributes.fetch_value(attr_name)) + end + + def clean_copy_of(attributes) + attributes.map do |attr| + attr.with_value_from_database(attr.value_for_database) + end + end + end + + class NullMutationTracker # :nodoc: + def changed_values + {} + end + + def changes + {} + end + + def changed?(*) + false + end + + def changed_in_place?(*) + false + end + + def forget_change(*) + end + end +end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 013a7d0e01..026b3cf014 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -60,8 +60,14 @@ module ActiveRecord super end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@attributes, attributes.deep_dup) + end + end + def initialize_dup(_) - @attributes = attributes.deep_dup + @attributes = attributes.dup super end @@ -80,6 +86,11 @@ module ActiveRecord attributes.select { |_, attr| attr.has_been_read? }.keys end + def map(&block) + new_attributes = attributes.transform_values(&block) + AttributeSet.new(new_attributes) + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 0c730d313f..f974b7a876 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -47,8 +47,14 @@ module ActiveRecord delegate_hash[key] = value end + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) + end + end + def initialize_dup(_) - @delegate_hash = delegate_hash.transform_values(&:dup) + @delegate_hash = Hash[delegate_hash] super end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9ea22ed798..2456b8ad8c 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -14,10 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - unless obj.is_a?(object_class) - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" - end + assert_valid_value(obj) YAML.dump obj end @@ -26,15 +23,19 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && yaml =~ /^---/ obj = YAML.load(yaml) - unless obj.is_a?(object_class) || obj.nil? - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" - end + assert_valid_value(obj) obj ||= object_class.new if object_class != Object obj end + def assert_valid_value(obj) + unless obj.nil? || obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + end + private def check_arity_of_constructor 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 6544b64f52..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 @@ -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/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 2c76dd1518..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 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index b944a8631c..a2f58364a4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -18,7 +18,7 @@ module ActiveRecord spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) end - # This can be overridden on a Adapter level basis to support other + # This can be overridden on an Adapter level basis to support other # extended datatypes (Example: Adding an array option in the # PostgreSQLAdapter) def prepare_column_options(column) 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 16aefc94ab..7e53f8958a 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) 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) 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. # @@ -474,7 +507,7 @@ module ActiveRecord raise NotImplementedError, "change_column_default is not implemented" end - # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag # indicates whether the value can be +NULL+. For example # # change_column_null(:users, :nickname, false) @@ -486,7 +519,7 @@ module ActiveRecord # allows them to be +NULL+ (drops the constraint). # # The method accepts an optional fourth argument to replace existing - # +NULL+s with some other value. Use that one when enabling the + # <tt>NULL</tt>s with some other value. Use that one when enabling the # constraint if needed, since otherwise those rows would not be valid. # # Please note the fourth argument does not set a column's default. @@ -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 @@ -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 597f0d0597..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: @@ -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 @@ -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 @@ -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 e4fcff8814..b3894481cc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -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/core.rb b/activerecord/lib/active_record/core.rb index ffce2173ec..894d18b79e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -296,7 +296,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - @attributes = self.class._default_attributes.dup + @attributes = self.class._default_attributes.deep_dup self.class.define_attribute_methods init_internals @@ -366,7 +366,7 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - @attributes = @attributes.dup + @attributes = @attributes.deep_dup @attributes.reset(self.class.primary_key) _run_initialize_callbacks diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 4902fcb1a2..10b5fcab24 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -118,7 +118,7 @@ module ActiveRecord elsif mapping.has_value?(value) mapping.key(value) else - raise ArgumentError, "'#{value}' is not a valid #{name}" + assert_valid_value(value) end end @@ -131,6 +131,12 @@ module ActiveRecord mapping.fetch(value, value) end + def assert_valid_value(value) + unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + protected attr_reader :name, :mapping @@ -204,30 +210,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/migration.rb b/activerecord/lib/active_record/migration.rb index 5bdb7213cd..0c50826c63 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -231,7 +231,7 @@ module ActiveRecord # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column - # +reference_name_id+ by default a integer. See + # +reference_name_id+ by default an integer. See # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. # # == Irreversible transformations diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 2b0c755ef4..a9bd094a66 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -50,6 +50,13 @@ module ActiveRecord class_attribute :pluralize_table_names, instance_writer: false self.pluralize_table_names = true + ## + # :singleton-method: + # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + class_attribute :ignored_columns, instance_accessor: false + self.ignored_columns = [].freeze + self.inheritance_column = 'type' delegate :type_for_attribute, to: :class @@ -213,7 +220,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 +297,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 @@ -308,7 +315,7 @@ module ActiveRecord end def load_schema! - @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) @columns_hash.each do |name, column| warn_if_deprecated_type(column) define_attribute( 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/persistence.rb b/activerecord/lib/active_record/persistence.rb index 09c36d7b4d..7b53f6e5a0 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -211,6 +211,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d543a84ff1..d940ac244a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -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 diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 4e0fc407bc..65ec356735 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -175,6 +175,20 @@ module ActiveRecord end end + def inverse_of + return unless inverse_name + + @inverse_of ||= klass._reflect_on_association inverse_name + end + + def check_validity_of_inverse! + unless polymorphic? + if has_inverse? && inverse_of.nil? + raise InverseOfAssociationNotFoundError.new(self) + end + end + end + # This shit is nasty. We need to avoid the following situation: # # * An associated record is deleted via record.destroy @@ -377,14 +391,6 @@ module ActiveRecord check_validity_of_inverse! end - def check_validity_of_inverse! - unless polymorphic? - if has_inverse? && inverse_of.nil? - raise InverseOfAssociationNotFoundError.new(self) - end - end - end - def check_preloadable! return unless scope @@ -436,12 +442,6 @@ module ActiveRecord inverse_name end - def inverse_of - return unless inverse_name - - @inverse_of ||= klass._reflect_on_association inverse_name - end - def polymorphic_inverse_of(associated_class) if has_inverse? if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]) @@ -924,6 +924,8 @@ module ActiveRecord klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end + def inverse_name; delegate_reflection.send(:inverse_name); end + private def derive_class_name # get the class_name of the belongs_to association of the through reflection 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..69f39e5ba9 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, @@ -338,7 +338,6 @@ module ActiveRecord # column_alias_for("sum(id)") # => "sum_id" # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" # column_alias_for("count(*)") # => "count_all" - # column_alias_for("count", "id") # => "count_id" def column_alias_for(keys) if keys.respond_to? :name keys = "#{keys.relation.name}.#{keys.name}" diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index e232516b0c..39e7b42629 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -67,7 +67,7 @@ module ActiveRecord # Arel::Nodes::And.new([range.start, range.end]) # ) # end - # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) + # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler) def register_handler(klass, handler) @handlers.unshift([klass, handler]) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index e25b889851..eb53a18f0f 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 @@ -264,7 +264,7 @@ module ActiveRecord # Returns an array with distinct records based on the +group+ attribute: # # User.select([:id, :name]) - # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">] # # User.group(:name) # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] @@ -713,7 +713,7 @@ module ActiveRecord # # users = User.readonly # users.first.save - # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord + # => ActiveRecord::ReadOnlyRecord: User is marked as readonly def readonly(value = true) spawn.readonly!(value) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index ba75ffa5a1..7f6664ea50 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -4,7 +4,7 @@ module ActiveRecord module ClassMethods # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: + def sanitize(object) # :nodoc: connection.quote(object) end alias_method :quote_value, :sanitize @@ -13,9 +13,16 @@ module ActiveRecord # Accepts an array or string of SQL conditions and sanitizes # 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) + # + # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" + # + # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") + # # => "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition) return nil if condition.blank? case condition @@ -28,7 +35,15 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. - # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'" + # + # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) + # # => "name=NULL and group_id=4" + # + # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) + # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" + # + # sanitize_sql_for_assignment("name=NULL and group_id='4'") + # # => "name=NULL and group_id='4'" def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) case assignments when Array; sanitize_sql_array(assignments) @@ -40,14 +55,18 @@ module ActiveRecord # Accepts a hash of SQL conditions and replaces those attributes # that correspond to a +composed_of+ relationship with their expanded # aggregate attribute values. + # # Given: - # class Person < ActiveRecord::Base - # composed_of :address, class_name: "Address", - # mapping: [%w(address_street street), %w(address_city city)] - # end + # + # class Person < ActiveRecord::Base + # composed_of :address, class_name: "Address", + # mapping: [%w(address_street street), %w(address_city city)] + # end + # # Then: - # { address: Address.new("813 abc st.", "chicago") } - # # => { address_street: "813 abc st.", address_city: "chicago" } + # + # { address: Address.new("813 abc st.", "chicago") } + # # => { address_street: "813 abc st.", address_city: "chicago" } def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| @@ -68,8 +87,9 @@ module ActiveRecord end # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { status: nil, group_id: 1 } - # # => "status = NULL , group_id = 1" + # + # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts") + # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1" def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| @@ -79,7 +99,19 @@ module ActiveRecord end # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". + # + # sanitize_sql_like("100%") + # # => "100\\%" + # + # sanitize_sql_like("snake_cased_string") + # # => "snake\\_cased\\_string" + # + # sanitize_sql_like("100%", "!") + # # => "100!%" + # + # sanitize_sql_like("snake_cased_string", "!") + # # => "snake!_cased!_string" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") string.gsub(pattern) { |x| [escape_character, x].join } @@ -87,7 +119,12 @@ module ActiveRecord # Accepts an array of conditions. The array has each value # sanitized and interpolated into the SQL statement. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # + # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) + # # => "name='foo''bar' and group_id=4" + # + # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) + # # => "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary if values.first.is_a?(Hash) && statement =~ /:\w+/ @@ -101,7 +138,7 @@ module ActiveRecord end end - def replace_bind_variables(statement, values) #:nodoc: + def replace_bind_variables(statement, values) # :nodoc: raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection @@ -110,7 +147,7 @@ module ActiveRecord end end - def replace_bind_variable(value, c = connection) #:nodoc: + def replace_bind_variable(value, c = connection) # :nodoc: if ActiveRecord::Relation === value value.to_sql else @@ -118,7 +155,7 @@ module ActiveRecord end end - def replace_named_bind_variables(statement, bind_vars) #:nodoc: + def replace_named_bind_variables(statement, bind_vars) # :nodoc: statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ':' # skip postgresql casts match # return the whole match @@ -130,7 +167,7 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) #:nodoc: + def quote_bound_value(value, c = connection) # :nodoc: if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) @@ -142,7 +179,7 @@ module ActiveRecord end end - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc: unless expected == provided raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index fd48ea402a..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 diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index ba5dd0a0d9..8929aa85c8 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -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/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4a569fc242..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 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..4ff0740cfb 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 @@ -41,6 +41,12 @@ module ActiveRecord ActiveRecord::Store::IndifferentHashAccessor end + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value) + end + end + private def default_value?(value) 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/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_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index d160c30375..20af436e02 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -95,6 +95,15 @@ class DeveloperWithExtendOption < Developer has_and_belongs_to_many :projects, extend: NamedExtension end +class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base + self.table_name = 'projects' + has_and_belongs_to_many :developers, -> { unscope(where: 'name') }, + class_name: "LazyBlockDeveloperCalledDavid", + join_table: "developers_projects", + foreign_key: "project_id", + association_foreign_key: "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -936,4 +945,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, professor.courses.count end + + def test_habtm_scope_can_unscope + project = ProjectUnscopingDavidDefaultScope.new + project.save! + + developer = LazyBlockDeveloperCalledDavid.new(name: "Not David") + developer.save! + project.developers << developer + + projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id) + assert_equal 1, projects.first.developers.size + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0beaf0056a..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 diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 423b8238b1..ece4dab539 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -13,6 +13,9 @@ require 'models/mixed_case_monkey' require 'models/admin' require 'models/admin/account' require 'models/admin/user' +require 'models/developer' +require 'models/company' +require 'models/project' class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club) assert_nil belongs_to_ref.inverse_of end + + def test_this_inverse_stuff + firm = Firm.create!(name: 'Adequate Holdings') + Project.create!(name: 'Project 1', firm: firm) + Developer.create!(name: 'Gorbypuff', firm: firm) + + new_project = Project.last + assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present" + assert new_project.lead_developer.present?, "Expected lead developer to be present on the project" + end end class InverseHasOneTests < ActiveRecord::TestCase 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_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 9d927481ec..5a0e463a48 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -29,7 +29,7 @@ module ActiveRecord assert_equal :bar, attributes[:bar].name end - test "duping creates a new hash and dups each attribute" do + test "duping creates a new hash, but does not dup the attributes" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) attributes = builder.build_from_database(foo: 1, bar: 'foo') @@ -43,6 +43,24 @@ module ActiveRecord assert_equal 1, attributes[:foo].value assert_equal 2, duped[:foo].value + assert_equal 'foobar', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "deep_duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.deep_dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value assert_equal 'foo', attributes[:bar].value assert_equal 'foobar', duped[:bar].value end @@ -160,6 +178,9 @@ module ActiveRecord return if value.nil? value + " from database" end + + def assert_valid_value(*) + end end test "write_from_database sets the attribute with database typecasting" do @@ -207,5 +228,16 @@ module ActiveRecord assert_equal [:foo], attributes.accessed end + + test "#map returns a new attribute set with the changes applied" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + new_attributes = attributes.map do |attr| + attr.with_cast_value(attr.value + 1) + end + + assert_equal 2, new_attributes.fetch_value(:foo) + assert_equal 3, new_attributes.fetch_value(:bar) + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 0ec368f51d..0f216b7a13 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 @@ -108,6 +107,9 @@ module ActiveRecord def deserialize(value) value + " from database" end + + def assert_valid_value(*) + end end test "with_value_from_user returns a new attribute with the value from the user" do @@ -187,5 +189,21 @@ module ActiveRecord assert_not attribute.changed_in_place_from?("bar") end + + test "with_value_from_user validates the value" do + type = Type::Value.new + type.define_singleton_method(:assert_valid_value) do |value| + if value == 1 + raise ArgumentError + end + end + + attribute = Attribute.from_database(:foo, 1, type) + assert_equal 1, attribute.value + assert_equal 2, attribute.with_value_from_user(2).value + assert_raises ArgumentError do + attribute.with_value_from_user(1) + end + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5a93ea3832..dbbcaa075d 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 @@ -1573,4 +1571,22 @@ class BasicsTest < ActiveRecord::TestCase assert_not topic.id_changed? end + + test "ignored columns are not present in columns_hash" do + cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name) + assert_includes cache_columns.keys, 'first_name' + refute_includes Developer.columns_hash.keys, 'first_name' + end + + test "ignored columns have no attribute methods" do + refute Developer.new.respond_to?(:first_name) + refute Developer.new.respond_to?(:first_name=) + refute Developer.new.respond_to?(:first_name?) + end + + test "ignored columns don't prevent explicit declaration of attribute methods" do + assert Developer.new.respond_to?(:last_name) + assert Developer.new.respond_to?(:last_name=) + assert Developer.new.respond_to?(:last_name?) + 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..cd1967c373 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -89,7 +89,7 @@ class DirtyTest < ActiveRecord::TestCase target = Class.new(ActiveRecord::Base) target.table_name = 'pirates' - pirate = target.create + pirate = target.create! pirate.created_on = pirate.created_on assert !pirate.created_on_changed? end @@ -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? @@ -578,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase end def test_datetime_attribute_can_be_updated_with_fractional_seconds + skip "Fractional seconds are not supported" unless subsecond_precision_supported? in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) target.table_name = 'topics' diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 0dc884497c..307b68764e 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -701,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 @@ -721,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/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/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/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_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/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 262e0abc22..14e392ac30 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -9,11 +9,11 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables 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/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 a429d06aad..d04f4f7ce7 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -168,4 +168,15 @@ class ValidationsTest < ActiveRecord::TestCase ensure Topic.reset_column_information end + + def test_acceptance_validator_doesnt_require_db_connection + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'posts' + end + klass.reset_column_information + + assert_no_queries do + klass.validates_acceptance_of(:foo) + end + end end 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/company.rb b/activerecord/test/models/company.rb index 67936e8e5d..a96b8ef0f2 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -86,6 +86,9 @@ class Firm < Company has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client' + has_one :lead_developer, class_name: "Developer" + has_many :projects + def log @log ||= [] end 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/models/developer.rb b/activerecord/test/models/developer.rb index 8ac7a9df6a..7c5941b1af 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -7,6 +7,8 @@ module DeveloperProjectsAssociationExtension2 end class Developer < ActiveRecord::Base + self.ignored_columns = %w(first_name last_name) + has_and_belongs_to_many :projects do def find_most_recent order("id DESC").first @@ -52,6 +54,9 @@ class Developer < ActiveRecord::Base has_many :ratings, through: :comments has_one :ship, dependent: :nullify + belongs_to :firm + has_many :contracted_projects, class_name: "Project" + scope :jamises, -> { where(:name => 'Jamis') } validates_inclusion_of :salary, :in => 50000..200000 @@ -61,6 +66,9 @@ class Developer < ActiveRecord::Base developer.audit_logs.build :message => "Computer created" end + attr_accessor :last_name + define_attribute_method 'last_name' + def log=(message) audit_logs.build :message => message end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 7f42a4b1f8..5328330653 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -11,6 +11,8 @@ class Project < ActiveRecord::Base :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, :after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"} has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer" + belongs_to :firm + has_one :lead_developer, through: :firm, inverse_of: :contracted_projects attr_accessor :developers_log after_initialize :set_developers_log diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 994ece9244..d334a2740e 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) @@ -251,11 +250,20 @@ ActiveRecord::Schema.define do create_table :developers, force: true do |t| t.string :name + t.string :first_name t.integer :salary, default: 70000 - t.datetime :created_at - t.datetime :updated_at - t.datetime :created_on - t.datetime :updated_on + t.integer :firm_id + 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 +362,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 +516,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 +538,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 +591,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| @@ -627,6 +659,7 @@ ActiveRecord::Schema.define do create_table :projects, force: true do |t| t.string :name t.string :type + t.integer :firm_id end create_table :randomly_named_table1, force: true do |t| @@ -686,7 +719,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 +793,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 +816,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 +985,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..a39344e464 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,7 +1,29 @@ +* `assert_difference` and `assert_no_difference` now returns the result of the + yielded block. + + Example: + + post = assert_difference -> { Post.count }, 1 do + Post.create + end + + *Lucas Mazza* + +* 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* - + * `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option to handle placement of delimiter, to support currency formats like INR @@ -266,20 +288,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 +308,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 80c5fdba17..3db9ea2ac0 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -536,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 @@ -686,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 @@ -764,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 c5cc31abc5..8718b7e1e5 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -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_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/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 2a9c09fc29..f59d05b214 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -40,6 +40,8 @@ class DateTime alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s) alias_method :to_s, :to_formatted_s + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24)) # datetime.formatted_offset # => "-06:00" diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index c30044b9ff..05679f1dd0 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,10 +1,14 @@ class Hash - # Returns a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the +block+ operation. # # hash = { name: 'Rob', age: '28' } # - # hash.transform_keys{ |key| key.to_s.upcase } - # # => {"NAME"=>"Rob", "AGE"=>"28"} + # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"} + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} def transform_keys return enum_for(:transform_keys) unless block_given? result = self.class.new @@ -14,8 +18,8 @@ class Hash result end - # Destructively converts all keys using the block operations. - # Same as transform_keys but modifies +self+. + # Destructively converts all keys using the +block+ operations. + # Same as +transform_keys+ but modifies +self+. def transform_keys! return enum_for(:transform_keys!) unless block_given? keys.each do |key| diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index e9bcce761f..9ddb838774 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -2,10 +2,15 @@ class Hash # Returns a new hash with the results of running +block+ once for every value. # The keys are unchanged. # - # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } - # # => { a: 2, b: 4, c: 6 } + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 } + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } def transform_values return enum_for(:transform_values) unless block_given? + return {} if empty? result = self.class.new each do |key, value| result[key] = yield(value) @@ -13,7 +18,8 @@ class Hash result end - # Destructive +transform_values+ + # Destructively converts all values using the +block+ operations. + # Same as +transform_values+ but modifies +self+. def transform_values! return enum_for(:transform_values!) unless block_given? each do |key, value| 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/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 a523e76b69..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. # diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index dbf1f2f373..24d13937f6 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -55,7 +55,8 @@ class Time alias_method :to_default_s, :to_s alias_method :to_s, :to_formatted_s - # Returns the UTC offset as an +HH:MM formatted string. + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # Time.local(2000).formatted_offset # => "-06:00" # Time.local(2000).formatted_offset(false) # => "-0600" 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/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/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index d87ce3474d..ae8c15d8bf 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -68,13 +68,15 @@ module ActiveSupport } before = exps.map(&:call) - yield + retval = yield expressions.zip(exps).each_with_index do |(code, e), i| error = "#{code.inspect} didn't change by #{difference}" error = "#{message}.\n#{error}" if message assert_equal(before[i] + difference, e.call, error) end + + retval end # Assertion that the numeric result of evaluating an expression is not diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 07fc787550..a82a7dfea0 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -14,7 +14,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00 - # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 + # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00 # Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # @@ -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 c8dbe92171..312cce2821 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' @@ -189,7 +189,7 @@ 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 @@ -285,8 +285,12 @@ module ActiveSupport end end - # Returns the offset of this time zone as a formatted string, of the - # format "+HH:MM". + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. + # + # zone = ActiveSupport::TimeZone['Central Time (US & Canada)'] + # zone.formatted_offset # => "-06:00" + # zone.formatted_offset(false) # => "-0600" def formatted_offset(colon=true, alternate_utc_string = nil) utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon) end 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/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb index a7e12117f3..5a0b99e22c 100644 --- a/activesupport/test/core_ext/hash/transform_keys_test.rb +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -24,9 +24,21 @@ class TransformKeysTest < ActiveSupport::TestCase assert_equal Enumerator, enumerator.class end + test "transform_keys! returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_keys! + assert_equal Enumerator, enumerator.class + end + test "transform_keys is chainable with Enumerable methods" do original = { a: 'a', b: 'b' } mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } assert_equal({ a0: 'a', b1: 'b' }, mapped) end + + test "transform_keys! is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + original.transform_keys!.with_index { |k, i| [k, i].join.to_sym } + assert_equal({ a0: 'a', b1: 'b' }, original) + end end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb index 45ed11fef7..7c33227dc0 100644 --- a/activesupport/test/core_ext/hash/transform_values_test.rb +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -53,9 +53,21 @@ class TransformValuesTest < ActiveSupport::TestCase assert_equal Enumerator, enumerator.class end + test "transform_values! returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_values! + assert_equal Enumerator, enumerator.class + end + test "transform_values is chainable with Enumerable methods" do original = { a: 'a', b: 'b' } mapped = original.transform_values.with_index { |v, i| [v, i].join } assert_equal({ a: 'a0', b: 'b1' }, mapped) end + + test "transform_values! is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + original.transform_values!.with_index { |v, i| [v, i].join } + assert_equal({ a: 'a0', b: 'b1' }, original) + 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/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 9e6d1a91d0..18228a2ac5 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -56,6 +56,14 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_assert_difference_retval + incremented = assert_difference '@object.num', +1 do + @object.increment + end + + assert_equal incremented, 1 + end + def test_assert_difference_with_implicit_difference assert_difference '@object.num' do @object.increment 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_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_validations.md b/guides/source/active_record_validations.md index dadac7fb54..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 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 5e72b96787..f0d87e4dc8 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. @@ -642,7 +641,7 @@ TIP: You don't have to update the database configurations manually. If you look ### Connection Preference -Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact. +Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact. If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable: diff --git a/guides/source/engines.md b/guides/source/engines.md index 3b1588b75a..71844b7990 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. @@ -843,28 +843,10 @@ above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: ```html+erb <p> <b>Author:</b> - <%= @article.author %> + <%= @article.author.name %> </p> ``` -By outputting `@article.author` using the `<%=` tag, the `to_s` method will be -called on the object. By default, this will look quite ugly: - -``` -#<User:0x00000100ccb3b0> -``` - -This is undesirable. It would be much better to have the user's name there. To -do this, add a `to_s` method to the `User` class within the application: - -```ruby -def to_s - name -end -``` - -Now instead of the ugly Ruby object output, the author's name will be displayed. - #### Using a Controller Provided by the Application Because Rails controllers generally share code for things like authentication diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 987320a0f8..ea79855919 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -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_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/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/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/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 ed0ea9dd4d..38004fe99a 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -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 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/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index dacab08ec3..1923433c4a 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -42,7 +42,8 @@ module Minitest ENV["RAILS_ENV"] = options[:environment] || "test" unless run_with_autorun - ::Rails::TestRequirer.require_files @rake_patterns || options[:patterns] + patterns = defined?(@rake_patterns) ? @rake_patterns : options[:patterns] + ::Rails::TestRequirer.require_files(patterns) end unless options[:full_backtrace] || ENV["BACKTRACE"] diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f677a7c42a..464fc1688c 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1304,7 +1304,7 @@ module ApplicationTests assert_equal 'custom key', Rails.application.config.my_custom_config['key'] end - test "config_for with syntax error show a more descritive exception" do + test "config_for with syntax error show a more descriptive exception" do app_file 'config/custom.yml', <<-RUBY development: key: foo: 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/application/routing_test.rb b/railties/test/application/routing_test.rb index cbada6be97..0777714d35 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -21,6 +21,12 @@ module ApplicationTests assert_equal 200, last_response.status end + test "rails/info in development" do + app("development") + get "/rails/info" + assert_equal 302, last_response.status + end + test "rails/info/routes in development" do app("development") get "/rails/info/routes" @@ -63,6 +69,12 @@ module ApplicationTests assert_equal 404, last_response.status end + test "rails/info in production" do + app("production") + get "/rails/info" + assert_equal 404, last_response.status + end + test "rails/info/routes in production" do app("production") get "/rails/info/routes" 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 diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 65d8a55421..41d36680e0 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -159,6 +159,7 @@ module TestHelpers require "rails" require "action_controller/railtie" require "action_view/railtie" + require 'action_dispatch/middleware/flash' app = Class.new(Rails::Application) app.config.eager_load = false |