Merge branch 'master' into feature/reselect-method

This commit is contained in:
Willian Gustavo Veiga 2018-10-17 20:24:44 -03:00
commit c8ff9bd63a
49 changed files with 518 additions and 347 deletions

View File

@ -31,7 +31,7 @@ addons:
- postgresql-10
- postgresql-client-10
bundler_args: --without test --jobs 3 --retry 3
bundler_args: --jobs 3 --retry 3
before_install:
- "rm ${BUNDLE_GEMFILE}.lock"
- "travis_retry gem update --system"

View File

@ -99,6 +99,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
gem "minitest-bisect"
gem "minitest-retry"
platforms :mri do
gem "stackprof"

View File

@ -306,7 +306,7 @@ GEM
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
@ -323,6 +323,8 @@ GEM
minitest-bisect (1.4.0)
minitest-server (~> 1.0)
path_expander (~> 1.0)
minitest-retry (0.1.9)
minitest (>= 5.0)
minitest-server (1.0.5)
minitest (~> 5.0)
mono_logger (1.1.0)
@ -538,6 +540,7 @@ DEPENDENCIES
libxml-ruby
listen (>= 3.0.5, < 3.2)
minitest-bisect
minitest-retry
mysql2 (>= 0.4.10)
nokogiri (>= 1.8.1)
pg (>= 0.18.0)
@ -576,4 +579,4 @@ DEPENDENCIES
websocket-client-simple!
BUNDLED WITH
1.16.5
1.16.6

View File

@ -152,9 +152,9 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(2, email.parts.length)
assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/html", email.parts[0].mime_type)
assert_equal("Attachment with content", email.parts[0].body.encoded)
assert_equal("Attachment with content", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type)
assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded)
assert_equal("This is test File content", email.parts[1].decoded)
end
test "adds the given :body as part" do
@ -162,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(2, email.parts.length)
assert_equal("multipart/mixed", email.mime_type)
assert_equal("text/plain", email.parts[0].mime_type)
assert_equal("I'm the eggman", email.parts[0].body.encoded)
assert_equal("I'm the eggman", email.parts[0].decoded)
assert_equal("application/pdf", email.parts[1].mime_type)
assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded)
assert_equal("This is test File content", email.parts[1].decoded)
end
test "can embed an inline attachment" do

View File

@ -1,3 +1,7 @@
* Add `ActionController::Parameters#each_value`.
*Lukáš Zapletal*
* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`.
*Yoshiyuki Kinjo*

View File

@ -352,7 +352,7 @@ module ActionController
# the same way as <tt>Hash#each_value</tt>.
def each_value(&block)
@parameters.each_pair do |key, value|
yield [convert_hashes_to_parameters(key, value)]
yield convert_hashes_to_parameters(key, value)
end
end

View File

@ -77,11 +77,15 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
test "each_value carries permitted status" do
@params.permit!
@params["person"].each_value { |value| assert(value.permitted?) if value == 32 }
@params.each_value do |value|
assert_predicate(value, :permitted?)
end
end
test "each_value carries unpermitted status" do
@params["person"].each_value { |value| assert_not(value.permitted?) if value == 32 }
@params.each_value do |value|
assert_not_predicate(value, :permitted?)
end
end
test "each_key converts to hash for permitted" do

View File

@ -31,7 +31,7 @@ module ActiveJob
# jobs. Since jobs share a single thread pool, long-running jobs will block
# short-lived jobs. Fine for dev/test; bad for production.
class AsyncAdapter
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
def initialize(**executor_options)
@scheduler = Scheduler.new(**executor_options)
end

View File

@ -197,7 +197,7 @@ module ActiveJob
# assert_performed_jobs 2
# end
#
# If a block is passed, that block should cause the specified number of
# If a block is passed, asserts that the block will cause the specified number of
# jobs to be performed.
#
# def test_jobs_again
@ -279,7 +279,7 @@ module ActiveJob
# end
# end
#
# If a block is passed, that block should not cause any job to be performed.
# If a block is passed, asserts that the block will not cause any job to be performed.
#
# def test_jobs_again
# assert_no_performed_jobs do
@ -347,7 +347,7 @@ module ActiveJob
# end
#
#
# If a block is passed, that block should cause the job to be
# If a block is passed, asserts that the block will cause the job to be
# enqueued with the given arguments.
#
# def test_assert_enqueued_with

View File

@ -474,5 +474,43 @@ module ActiveModel
def _read_attribute(attr)
__send__(attr)
end
module AttrNames # :nodoc:
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def self.define_attribute_accessor_method(mod, attr_name, writer: false)
method_name = "#{attr_name}#{'=' if writer}"
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
yield method_name, "'#{attr_name}'.freeze"
else
safe_name = attr_name.unpack1("h*")
const_name = "ATTR_#{safe_name}"
const_set(const_name, attr_name) unless const_defined?(const_name)
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
yield temp_method_name, attr_name_expr
mod.send(:alias_method, method_name, temp_method_name)
mod.send(:undef_method, temp_method_name)
end
end
end
end
end

View File

@ -29,17 +29,16 @@ module ActiveModel
private
def define_method_attribute=(name)
safe_name = name.unpack1("h*")
ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
write_attribute(name, value)
end
RUBY
end
end
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
@ -97,15 +96,4 @@ module ActiveModel
write_attribute(attribute_name, value)
end
end
module AttributeMethods #:nodoc:
AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}
end
end

View File

@ -1,4 +1,12 @@
* Add `reselect` method. This is short-hand for `unscope(:select).select(fields)`.
* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error.
Fixes #33056.
*Federico Martinez*
* Add `reselect` method. This is a short-hand for `unscope(:select).select(fields)`.
Fixes #27340.
*Willian Gustavo Veiga*

View File

@ -22,15 +22,6 @@ module ActiveRecord
delegate :column_for_attribute, to: :class
end
AttrNames = Module.new {
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
const_set const_name, -value
end
end
}
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
class GeneratedAttributeMethods < Module #:nodoc:
@ -270,21 +261,14 @@ module ActiveRecord
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path"
when :to_model
name = "to_model"
else
name = name.to_s
end
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
if defined?(@attributes) && self.class.column_names.include?(name)
return has_attribute?(name)
if defined?(@attributes)
if name = self.class.symbol_column_to_string(name.to_sym)
return has_attribute?(name)
end
end
true

View File

@ -8,42 +8,19 @@ module ActiveRecord
module ClassMethods # :nodoc:
private
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
# Evaluating many similar methods may use more memory as the instruction
# sequences are duplicated and cached (in MRI). define_method may
# be slower on dispatch, but if you're careful about the closure
# created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def define_method_attribute(name)
safe_name = name.unpack1("h*")
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def #{temp_method}
#{sync_with_transaction_state}
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
STR
generated_attribute_methods.module_eval do
alias_method name, temp_method
undef_method temp_method
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}
#{sync_with_transaction_state}
name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
RUBY
end
end
end

View File

@ -13,19 +13,19 @@ module ActiveRecord
private
def define_method_attribute=(name)
safe_name = name.unpack1("h*")
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
RUBY
end
end
end

View File

@ -20,9 +20,9 @@ module ActiveRecord
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
if collection.has_limit_or_offset?
query = collection.select(column)
query = collection.select("#{column} AS collection_cache_key_timestamp")
subquery_alias = "subquery_for_cache_key"
subquery_column = "#{subquery_alias}.#{timestamp_column}"
subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
subquery = query.arel.as(subquery_alias)
arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
else

View File

@ -348,8 +348,8 @@ module ActiveRecord
#
# create_table :taggings do |t|
# t.references :tag, index: { name: 'index_taggings_on_tag_id' }
# t.references :tagger, polymorphic: true, index: true
# t.references :taggable, polymorphic: { default: 'Photo' }
# t.references :tagger, polymorphic: true
# t.references :taggable, polymorphic: { default: 'Photo' }, index: false
# end
def column(name, type, options = {})
name = name.to_s

View File

@ -846,17 +846,17 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
# ====== Create a user_id bigint column
# ====== Create a user_id bigint column without a index
#
# add_reference(:products, :user)
# add_reference(:products, :user, index: false)
#
# ====== Create a user_id string column
#
# add_reference(:products, :user, type: :string)
#
# ====== Create supplier_id, supplier_type columns and appropriate index
# ====== Create supplier_id, supplier_type columns
#
# add_reference(:products, :supplier, polymorphic: true, index: true)
# add_reference(:products, :supplier, polymorphic: true)
#
# ====== Create a supplier_id column with a unique index
#
@ -884,7 +884,7 @@ module ActiveRecord
#
# ====== Remove the reference
#
# remove_reference(:products, :user, index: true)
# remove_reference(:products, :user, index: false)
#
# ====== Remove polymorphic reference
#
@ -892,7 +892,7 @@ module ActiveRecord
#
# ====== Remove the reference with a foreign key
#
# remove_reference(:products, :user, index: true, foreign_key: true)
# remove_reference(:products, :user, foreign_key: true)
#
def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options)
if foreign_key

View File

@ -125,6 +125,8 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
check_version
end
def replica?
@ -502,6 +504,9 @@ module ActiveRecord
end
private
def check_version
end
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping)

View File

@ -54,10 +54,6 @@ module ActiveRecord
super(connection, logger, config)
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
if version < "5.5.8"
raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
def version #:nodoc:
@ -535,6 +531,12 @@ module ActiveRecord
end
private
def check_version
if version < "5.5.8"
raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
def combine_multi_statements(total_sql)
total_sql.each_with_object([]) do |sql, total_sql_chunks|
previous_packet = total_sql_chunks.last

View File

@ -43,9 +43,14 @@ module ActiveRecord
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
conn_params.slice!(*valid_conn_param_keys)
# The postgres drivers don't allow the creation of an unconnected PG::Connection object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
conn = PG.connect(conn_params)
ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
rescue ::PG::Error => error
if error.message.include?("does not exist")
raise ActiveRecord::NoDatabaseError
else
raise
end
end
end
@ -220,15 +225,11 @@ module ActiveRecord
@local_tz = nil
@max_identifier_length = nil
connect
configure_connection
add_pg_encoders
@statements = StatementPool.new @connection,
self.class.type_cast_config_to_integer(config[:statement_limit])
if postgresql_version < 90100
raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
end
add_pg_decoders
@type_map = Type::HashLookupTypeMap.new
@ -410,6 +411,12 @@ module ActiveRecord
end
private
def check_version
if postgresql_version < 90100
raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
end
end
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@ -699,12 +706,6 @@ module ActiveRecord
def connect
@connection = PG.connect(@connection_parameters)
configure_connection
rescue ::PG::Error => error
if error.message.include?("does not exist")
raise ActiveRecord::NoDatabaseError
else
raise
end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.

View File

@ -105,11 +105,6 @@ module ActiveRecord
@active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
if sqlite_version < "3.8.0"
raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
end
configure_connection
end
@ -401,6 +396,12 @@ module ActiveRecord
end
private
def check_version
if sqlite_version < "3.8.0"
raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
end
end
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(int)i, SQLite3Integer

View File

@ -344,34 +344,22 @@ module ActiveRecord
# post = Post.allocate
# post.init_with(coder)
# post.title # => 'hello world'
def init_with(coder)
def init_with(coder, &block)
coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = self.class.yaml_encoder.decode(coder)
init_internals
@new_record = coder["new_record"]
self.class.define_attribute_methods
yield self if block_given?
_run_find_callbacks
_run_initialize_callbacks
self
attributes = self.class.yaml_encoder.decode(coder)
init_with_attributes(attributes, coder["new_record"], &block)
end
##
# Initializer used for instantiating objects that have been read from the
# database. +attributes+ should be an attributes object, and unlike the
# Initialize an empty model object from +attributes+.
# +attributes+ should be an attributes object, and unlike the
# `initialize` method, no assignment calls are made per attribute.
#
# :nodoc:
def init_from_db(attributes)
def init_with_attributes(attributes, new_record = false)
init_internals
@new_record = false
@new_record = new_record
@attributes = attributes
self.class.define_attribute_methods

View File

@ -388,6 +388,11 @@ module ActiveRecord
@column_names ||= columns.map(&:name)
end
def symbol_column_to_string(name_symbol) # :nodoc:
@symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym)
@symbol_column_to_string_name_hash[name_symbol]
end
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
@ -477,6 +482,7 @@ module ActiveRecord
def reload_schema_from_cache
@arel_table = nil
@column_names = nil
@symbol_column_to_string_name_hash = nil
@attribute_types = nil
@content_columns = nil
@default_attributes = nil

View File

@ -426,7 +426,7 @@ module ActiveRecord
existing_record.assign_attributes(assignable_attributes)
association(association_name).initialize_attributes(existing_record)
else
method = "build_#{association_name}"
method = :"build_#{association_name}"
if respond_to?(method)
send(method, assignable_attributes)
else

View File

@ -209,7 +209,7 @@ module ActiveRecord
# new instance of the class. Accepts only keys as strings.
def instantiate_instance_of(klass, attributes, column_types = {}, &block)
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
klass.allocate.init_from_db(attributes, &block)
klass.allocate.init_with_attributes(attributes, &block)
end
# Called by +instantiate+ to decide which class to use for a new

View File

@ -25,6 +25,8 @@ require "models/user"
require "models/member"
require "models/membership"
require "models/sponsor"
require "models/lesson"
require "models/student"
require "models/country"
require "models/treaty"
require "models/vertex"
@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_habtm_saving_multiple_relationships
new_project = Project.new("name" => "Grimetime")
amount_of_developers = 4
developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse
developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") }
new_project.developer_ids = [developers[0].id, developers[1].id]
new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
@ -780,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_singular_ids_are_reloaded_after_collection_concat
student = Student.create(name: "Alberto Almagro")
student.lesson_ids
lesson = Lesson.create(name: "DSI")
student.lessons << lesson
assert_includes student.lesson_ids, lesson.id
end
def test_scoped_find_on_through_association_doesnt_return_read_only_records
tag = Post.find(1).tags.find_by_name("General")

View File

@ -42,6 +42,20 @@ module ActiveRecord
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
test "cache_key for relation with custom select and limit" do
developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5)
developers_with_select = developers.select("developers.*")
last_developer_timestamp = developers.first.updated_at
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key)
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key
assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
test "cache_key for loaded relation" do
developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load
last_developer_timestamp = developers.first.updated_at

View File

@ -1,3 +1,26 @@
* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`.
*Francesco Rodríguez*
* Fix duration being rounded to a full second.
```
time = DateTime.parse("2018-1-1")
time += 0.51.seconds
```
Will now correctly add 0.51 second and not 1 full second.
*Edouard Chin*
* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize`
in favor of `String#unicode_normalize`
*Francesco Rodríguez*
* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of
`String#downcase/upcase/swapcase`.
*Francesco Rodríguez*
* Add `ActiveSupport::ParameterFilter`.
*Yoshiyuki Kinjo*

View File

@ -411,8 +411,6 @@ module ActiveSupport
# to the cache. If you do not want to write the cache when the cache is
# not found, use #read_multi.
#
# Options are passed to the underlying cache implementation.
#
# Returns a hash with the data for each of the names. For example:
#
# cache.write("bim", "bam")
@ -422,6 +420,17 @@ module ActiveSupport
# # => { "bim" => "bam",
# # "unknown_key" => "Fallback value for key: unknown_key" }
#
# Options are passed to the underlying cache implementation. For example:
#
# cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
# "buzz"
# end
# # => {"fizz"=>"buzz"}
# cache.read("fizz")
# # => "buzz"
# sleep(6)
# cache.read("fizz")
# # => nil
def fetch_multi(*names)
raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?

View File

@ -110,7 +110,7 @@ class DateTime
# instance time. Do not use this method in combination with x.months, use
# months_since instead!
def since(seconds)
self + Rational(seconds.round, 86400)
self + Rational(seconds, 86400)
end
alias :in :since

View File

@ -62,9 +62,9 @@ module ActiveSupport
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
I18n.transliterate(
ActiveSupport::Multibyte::Unicode.normalize(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
replacement: replacement)
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
replacement: replacement
)
end
# Replaces special characters in a string so that it may be used as part of

View File

@ -17,7 +17,7 @@ module ActiveSupport #:nodoc:
# through the +mb_chars+ method. Methods which would normally return a
# String object now return a Chars object so methods can be chained.
#
# 'The Perfect String '.mb_chars.downcase.strip.normalize
# 'The Perfect String '.mb_chars.downcase.strip
# # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
#
# Chars objects are perfectly interchangeable with String objects as long as
@ -76,6 +76,11 @@ module ActiveSupport #:nodoc:
# Returns +true+ when the proxy class can handle the string. Returns
# +false+ otherwise.
def self.consumes?(string)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
removed from Rails 6.1. Use string.is_utf8? instead.
MSG
string.encoding == Encoding::UTF_8
end
@ -108,7 +113,7 @@ module ActiveSupport #:nodoc:
#
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
def reverse
chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack("U*"))
chars(@wrapped_string.scan(/\X/).reverse.join)
end
# Limits the byte size of the string to a number of bytes without breaking
@ -120,40 +125,12 @@ module ActiveSupport #:nodoc:
slice(0...translate_offset(limit))
end
# Converts characters in the string to uppercase.
#
# 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
def upcase
chars Unicode.upcase(@wrapped_string)
end
# Converts characters in the string to lowercase.
#
# 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
def downcase
chars Unicode.downcase(@wrapped_string)
end
# Converts characters in the string to the opposite case.
#
# 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN"
def swapcase
chars Unicode.swapcase(@wrapped_string)
end
# Converts the first character to uppercase and the remainder to lowercase.
#
# 'über'.mb_chars.capitalize.to_s # => "Über"
def capitalize
(slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase
end
# Capitalizes the first letter of every word, when possible.
#
# "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
# "日本語".mb_chars.titleize.to_s # => "日本語"
def titleize
chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) })
chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
end
alias_method :titlecase, :titleize
@ -165,7 +142,24 @@ module ActiveSupport #:nodoc:
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
# ActiveSupport::Multibyte::Unicode.default_normalization_form
def normalize(form = nil)
chars(Unicode.normalize(@wrapped_string, form))
form ||= Unicode.default_normalization_form
# See https://www.unicode.org/reports/tr15, Table 1
if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
MSG
send(:unicode_normalize, alias_form)
else
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
removed from Rails 6.1. Use #unicode_normalize instead.
MSG
raise ArgumentError, "#{form} is not a valid normalization variant", caller
end
end
# Performs canonical decomposition on all the characters.
@ -189,7 +183,7 @@ module ActiveSupport #:nodoc:
# 'क्षि'.mb_chars.length # => 4
# 'क्षि'.mb_chars.grapheme_length # => 3
def grapheme_length
Unicode.unpack_graphemes(@wrapped_string).length
@wrapped_string.scan(/\X/).length
end
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
@ -205,7 +199,7 @@ module ActiveSupport #:nodoc:
to_s.as_json(options)
end
%w(capitalize downcase reverse tidy_bytes upcase).each do |method|
%w(reverse tidy_bytes).each do |method|
define_method("#{method}!") do |*args|
@wrapped_string = send(method, *args).to_s
self

View File

@ -6,10 +6,17 @@ module ActiveSupport
extend self
# A list of all available normalization forms.
# See http://www.unicode.org/reports/tr15/tr15-29.html for more
# See https://www.unicode.org/reports/tr15/tr15-29.html for more
# information about normalization.
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
NORMALIZATION_FORM_ALIASES = { # :nodoc:
c: :nfc,
d: :nfd,
kc: :nfkc,
kd: :nfkd
}
# The Unicode version that is supported by the implementation
UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
@ -100,31 +107,34 @@ module ActiveSupport
# Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
def normalize(string, form = nil)
form ||= @default_normalization_form
# See http://www.unicode.org/reports/tr15, Table 1
case form
when :d
string.unicode_normalize(:nfd)
when :c
string.unicode_normalize(:nfc)
when :kd
string.unicode_normalize(:nfkd)
when :kc
string.unicode_normalize(:nfkc)
# See https://www.unicode.org/reports/tr15, Table 1
if alias_form = NORMALIZATION_FORM_ALIASES[form]
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead.
MSG
string.unicode_normalize(alias_form)
else
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be
removed from Rails 6.1. Use String#unicode_normalize instead.
MSG
raise ArgumentError, "#{form} is not a valid normalization variant", caller
end
end
def downcase(string)
string.downcase
end
%w(downcase upcase swapcase).each do |method|
define_method(method) do |string|
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode##{method} is deprecated and
will be removed from Rails 6.1. Use String methods directly.
MSG
def upcase(string)
string.upcase
end
def swapcase(string)
string.swapcase
string.send(method)
end
end
private

View File

@ -152,8 +152,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600)
assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2)
assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25)
assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333)
assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667)
assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333)
assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667)
end
def test_change

View File

@ -157,6 +157,16 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal({ "foo" => "hello" }, JSON.parse(json))
end
def test_struct_to_json_with_options_nested
klass = Struct.new(:foo, :bar)
struct = klass.new "hello", "world"
parent_struct = klass.new struct, "world"
json = parent_struct.to_json only: [:foo]
assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json))
end
def test_hash_should_pass_encoding_options_to_children_in_as_json
person = {
name: "John",

View File

@ -73,9 +73,15 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_consumes_utf8_strings
assert @proxy_class.consumes?(UNICODE_STRING)
assert @proxy_class.consumes?(ASCII_STRING)
assert_not @proxy_class.consumes?(BYTE_STRING)
ActiveSupport::Deprecation.silence do
assert @proxy_class.consumes?(UNICODE_STRING)
assert @proxy_class.consumes?(ASCII_STRING)
assert_not @proxy_class.consumes?(BYTE_STRING)
end
end
def test_consumes_is_deprecated
assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) }
end
def test_concatenation_should_return_a_proxy_class_instance
@ -165,7 +171,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
ActiveSupport::Deprecation.silence do
assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class)
end
assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class)
assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class)
@ -383,10 +391,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_reverse_should_work_with_normalized_strings
str = "bös"
reversed_str = "söb"
assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse
assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse
assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse
assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse
ActiveSupport::Deprecation.silence do
assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse
assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse
assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse
assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse
end
assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse
assert_equal chars(reversed_str).compose, chars(str).compose.reverse
end
@ -477,7 +487,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
def test_method_works_for_proxyed_methods
assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars
chars = "hello".mb_chars
chars = +"hello".mb_chars
assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars
assert_equal "Hello", chars
assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String
@ -568,7 +578,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
def test_composition_exclusion_is_set_up_properly
# Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly
qa = [0x915, 0x93c].pack("U*")
assert_equal qa, chars(qa).normalize(:c)
ActiveSupport::Deprecation.silence do
assert_equal qa, chars(qa).normalize(:c)
end
end
# Test for the Public Review Issue #29, bad explanation of composition might lead to a
@ -578,17 +590,21 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
[0x0B47, 0x0300, 0x0B3E],
[0x1100, 0x0300, 0x1161]
].map { |c| c.pack("U*") }.each do |c|
assert_equal_codepoints c, chars(c).normalize(:c)
ActiveSupport::Deprecation.silence do
assert_equal_codepoints c, chars(c).normalize(:c)
end
end
end
def test_normalization_shouldnt_strip_null_bytes
null_byte_str = "Test\0test"
assert_equal null_byte_str, chars(null_byte_str).normalize(:kc)
assert_equal null_byte_str, chars(null_byte_str).normalize(:c)
assert_equal null_byte_str, chars(null_byte_str).normalize(:d)
assert_equal null_byte_str, chars(null_byte_str).normalize(:kd)
ActiveSupport::Deprecation.silence do
assert_equal null_byte_str, chars(null_byte_str).normalize(:kc)
assert_equal null_byte_str, chars(null_byte_str).normalize(:c)
assert_equal null_byte_str, chars(null_byte_str).normalize(:d)
assert_equal null_byte_str, chars(null_byte_str).normalize(:kd)
end
assert_equal null_byte_str, chars(null_byte_str).decompose
assert_equal null_byte_str, chars(null_byte_str).compose
end
@ -601,11 +617,13 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
323 # COMBINING DOT BELOW
].pack("U*")
assert_equal_codepoints "", chars("").normalize
assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s
assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s
assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s
assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s
ActiveSupport::Deprecation.silence do
assert_equal_codepoints "", chars("").normalize
assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s
assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s
assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s
assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s
end
end
def test_should_compute_grapheme_length
@ -719,6 +737,41 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars
end
def test_unicode_normalize_deprecation
# String#unicode_normalize default form is `:nfc`, and
# different than Multibyte::Unicode default, `:nkfc`.
# Deprecation should suggest the right form if no params
# are given and default is used.
assert_deprecated(/unicode_normalize\(:nfkc\)/) do
ActiveSupport::Multibyte::Unicode.normalize("")
end
assert_deprecated(/unicode_normalize\(:nfd\)/) do
ActiveSupport::Multibyte::Unicode.normalize("", :d)
end
end
def test_chars_normalize_deprecation
# String#unicode_normalize default form is `:nfc`, and
# different than Multibyte::Unicode default, `:nkfc`.
# Deprecation should suggest the right form if no params
# are given and default is used.
assert_deprecated(/unicode_normalize\(:nfkc\)/) do
"".mb_chars.normalize
end
assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) }
assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) }
assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) }
assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) }
end
def test_unicode_deprecations
assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") }
assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") }
assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") }
end
private
def string_from_classes(classes)

View File

@ -18,64 +18,72 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
end
def test_normalizations_C
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
# CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations
#
# NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
#
# c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
# CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations
#
# NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
#
# c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
end
end
end
def test_normalizations_D
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
#
# NFD
# c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
# c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
#
# NFD
# c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
# c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
end
end
end
def test_normalizations_KC
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKC
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKC
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
end
end
end
def test_normalizations_KD
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKD
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKD
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
end
end
end

View File

@ -18,64 +18,72 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase
end
def test_normalizations_C
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
# CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations
#
# NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
#
# c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
# CONFORMANCE:
# 1. The following invariants must be true for all conformant implementations
#
# NFC
# c2 == NFC(c1) == NFC(c2) == NFC(c3)
assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
#
# c4 == NFC(c4) == NFC(c5)
assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
end
end
end
def test_normalizations_D
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
#
# NFD
# c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
# c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do |*cols|
col1, col2, col3, col4, col5, comment = *cols
#
# NFD
# c3 == NFD(c1) == NFD(c2) == NFD(c3)
assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
# c5 == NFD(c4) == NFD(c5)
assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
end
end
end
def test_normalizations_KC
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKC
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKC
# c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
end
end
end
def test_normalizations_KD
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKD
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
ActiveSupport::Deprecation.silence do
each_line_of_norm_tests do | *cols |
col1, col2, col3, col4, col5, comment = *cols
#
# NFKD
# c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
end
end
end

View File

@ -166,7 +166,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac
The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL:
```ruby
get '/clients/:status' => 'clients#index', foo: 'bar'
get '/clients/:status', to: 'clients#index', foo: 'bar'
```
In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients".

View File

@ -82,9 +82,9 @@ of two or more words, the model class name should follow the Ruby conventions,
using the CamelCase form, while the table name must contain the words separated
by underscores. Examples:
* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
* Model Class - Singular with the first letter of each word capitalized (e.g.,
`BookClub`).
* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
| Model / Class | Table / Schema |
| ---------------- | -------------- |

View File

@ -109,7 +109,7 @@ class CreateBooks < ActiveRecord::Migration[5.0]
end
create_table :books do |t|
t.belongs_to :author, index: true
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
@ -140,7 +140,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
t.belongs_to :supplier, index: true
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
@ -184,7 +184,7 @@ class CreateAuthors < ActiveRecord::Migration[5.0]
end
create_table :books do |t|
t.belongs_to :author, index: true
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
@ -231,8 +231,8 @@ class CreateAppointments < ActiveRecord::Migration[5.0]
end
create_table :appointments do |t|
t.belongs_to :physician, index: true
t.belongs_to :patient, index: true
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
@ -312,13 +312,13 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
t.belongs_to :supplier, index: true
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account, index: true
t.belongs_to :account
t.integer :credit_rating
t.timestamps
end
@ -358,8 +358,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly, index: true
t.belongs_to :part, index: true
t.belongs_to :assembly
t.belongs_to :part
end
end
end
@ -487,7 +487,7 @@ class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true, index: true
t.references :imageable, polymorphic: true
t.timestamps
end
end
@ -517,7 +517,7 @@ In your migrations/schema, you will add a references column to the model itself.
class CreateEmployees < ActiveRecord::Migration[5.0]
def change
create_table :employees do |t|
t.references :manager, index: true
t.references :manager
t.timestamps
end
end

View File

@ -1266,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering
In this case, the partial will have access to a local variable `title` with the value "Products Page".
TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view.
TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. Note that it also applies for when the partial name was changed by using the `as:` option. For example, the counter variable for the code above would be `item_counter`.
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:

View File

@ -1,3 +1,9 @@
* Use Ids instead of memory addresses when displaying references in scaffold views.
Fixes #29200.
*Rasesh Patel*
* Adds support for multiple databases to `rails db:migrate:status`.
Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`).

View File

@ -16,7 +16,7 @@
<%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
<tr>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
<td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
<% end -%>
<td><%%= link_to 'Show', <%= model_resource_name %> %></td>
<td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td>

View File

@ -3,7 +3,7 @@
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<p>
<strong><%= attribute.human_name %>:</strong>
<%%= @<%= singular_table_name %>.<%= attribute.name %> %>
<%%= @<%= singular_table_name %>.<%= attribute.column_name %> %>
</p>
<% end -%>

View File

@ -209,6 +209,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_new_application_doesnt_need_defaults
run_generator
assert_no_file "config/initializers/new_framework_defaults_6_0.rb"
end

View File

@ -435,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
def test_scaffold_generator_belongs_to
run_generator ["account", "name", "currency:belongs_to"]
def test_scaffold_generator_belongs_to_and_references
run_generator ["account", "name", "currency:belongs_to", "user:references"]
assert_file "app/models/account.rb", /belongs_to :currency/
@ -449,7 +449,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "app/controllers/accounts_controller.rb" do |content|
assert_instance_method :account_params, content do |m|
assert_match(/permit\(:name, :currency_id\)/, m)
assert_match(/permit\(:name, :currency_id, :user_id\)/, m)
end
end
@ -457,6 +457,16 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/^\W{4}<%= form\.text_field :name %>/, content)
assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content)
end
assert_file "app/views/accounts/index.html.erb" do |content|
assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content)
assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content)
end
assert_file "app/views/accounts/show.html.erb" do |content|
assert_match(/^\W{2}<%= @account\.name %>/, content)
assert_match(/^\W{2}<%= @account\.user_id %>/, content)
end
end
def test_scaffold_generator_database

View File

@ -16,6 +16,9 @@ require "active_support/testing/autorun"
require "active_support/testing/stream"
require "active_support/testing/method_call_assertions"
require "active_support/test_case"
require "minitest/retry"
Minitest::Retry.use!(verbose: false, retry_count: 1)
RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__)