`bind_call(obj, ...)` is a faster alternative to `bind(obj).call(...)`.
https://bugs.ruby-lang.org/issues/15955
Also, enable `Performance/BindCall` cop to detect those in the future.
Commit: 9ca7389272
Discussion: https://bugs.ruby-lang.org/issues/14473
It seems to be compatible with the original ActiveSupport's
implementation, at least based on the test suite.
It also works faster:
```
Warming up --------------------------------------
Ruby's Range#cover? 1.196M i/100ms
ActiveSupport's Range#cover?
396.369k i/100ms
Calculating -------------------------------------
Ruby's Range#cover? 11.889M (± 1.7%) i/s - 59.820M in 5.033066s
ActiveSupport's Range#cover?
3.951M (± 1.2%) i/s - 19.818M in 5.017176s
Comparison:
Ruby's Range#cover?: 11888979.3 i/s
ActiveSupport's Range#cover?: 3950671.0 i/s - 3.01x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
module ActiveSupportRange
def active_support_cover?(value)
if value.is_a?(::Range)
is_backwards_op = value.exclude_end? ? :>= : :>
return false if value.begin && value.end && value.begin.public_send(is_backwards_op, value.end)
# 1...10 covers 1..9 but it does not cover 1..10.
# 1..10 covers 1...11 but it does not cover 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
cover?(value.first) && (self.end.nil? || value_max.public_send(operator, last))
else
cover?
end
end
end
class BugTest < Minitest::Test
def test_range_cover
Range.prepend(ActiveSupportRange)
range = (1..10000)
Benchmark.ips do |x|
x.report("Ruby's Range#cover?") do
range.cover?((100..20))
end
x.report("ActiveSupport's Range#cover?") do
range.active_support_cover?((100..20))
end
x.compare!
end
end
end
```
The method signature has changed in Ruby 2.6:
https://bugs.ruby-lang.org/issues/14944
Rails 7.0 requires Ruby 2.7+ now and these checks/monkey patches
shouldn't be needed.
These methods have changed in Ruby 2.5 to be more akin to grep:
https://bugs.ruby-lang.org/issues/11286
Using classes seems to be faster (and a bit more expressive) than iterating over
the collection items:
```
Warming up --------------------------------------
#all? with class 504.000 i/100ms
#all? with proc 189.000 i/100ms
Calculating -------------------------------------
#all? with class 4.960k (± 1.6%) i/s - 25.200k in 5.082049s
#all? with proc 1.874k (± 2.8%) i/s - 9.450k in 5.047866s
Comparison:
#all? with class: 4959.9 i/s
#all? with proc: 1873.8 i/s - 2.65x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
class BugTest < Minitest::Test
def test_enumerators_with_classes
arr = (1..10000).to_a << nil
assert_equal arr.all?(Integer), arr.all? { |v| v.is_a?(Integer) }
Benchmark.ips do |x|
x.report("#all? with class") do
arr.all?(Integer)
end
x.report("#all? with proc") do
arr.all? { |v| v.is_a?(Integer) }
end
x.compare!
end
end
end
```
Both methods were introduced in Ruby 2.5 and are faster than scanning
unicode graphemes with String#scan
```
Warming up --------------------------------------
scan(/X/) 43.127k i/100ms
grapheme_clusters 103.348k i/100ms
Calculating -------------------------------------
scan(/X/) 427.853k (± 2.4%) i/s - 2.156M in 5.042967s
grapheme_clusters 1.045M (± 0.8%) i/s - 5.271M in 5.042360s
Comparison:
grapheme_clusters: 1045353.5 i/s
scan(/X/): 427852.8 i/s - 2.44x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
class BugTest < Minitest::Test
def test_grapheme_clusters
string = [0x0924, 0x094D, 0x0930].pack("U*") # "त्र"
# string = [0x000D, 0x000A].pack("U*") # cr lf
# string = "こにちわ"
assert string.scan(/\X/) == string.grapheme_clusters
Benchmark.ips do |x|
x.report("scan(/\X/)") do
string.scan(/\X/)
end
x.report("grapheme_clusters") do
string.grapheme_clusters
end
x.compare!
end
end
end
```
String#grapheme_clusters had a bug with CRLF which was fixed in Ruby
2.6: https://bugs.ruby-lang.org/issues/15337
Now that Rails requires Ruby 2.7+, it shouldn't be an issue.
* Add Enumerable#in_order_of
* Explain behavior further
* Use Enumerable#in_order_of
* Use Ruby 2.7 #filter_map
* Update activesupport/lib/active_support/core_ext/enumerable.rb
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
* No need for explaining variable
* Add CHANGELOG entry
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
Better implementation of https://github.com/rails/rails/pull/41313
Now, only `false` and `:lower` as args will return a downcased first character (before https://github.com/rails/rails/pull/41313 only `false` did). Everything else returns an upcased first character, which is the default.
Update activesupport/lib/active_support/inflector/methods.rb
Co-authored-by: Ryuta Kamizono <kamipo@gmail.com>
This was raised on the discussion forum: https://discuss.rubyonrails.org/t/inconsistency-between-string-camelize-and-activesupportinflector-camelize
`String#camelize` takes a symbol (`:upper` or `:lower`) as an argument. But `ActiveSupport::Inflector.camelize` takes a bool. This can result in surprising behavior if you assume the methods are interchangeable, and call `ActiveSupport::Inflector.camelize('active_support', :lower)` (the `:lower` argument is truthy so the return value is upcased).
This PR changes `ActiveSupport::Inflector.camelize` to match `String#camelize` behavior. It will now return an upcased string if you provide `true` or `:upper` as an argument, otherwise it will be downcased.
There a mixed bag of small optimizations here:
- Since underscores are converted to spaces and that we remove all leading underscores. By changing the order of operations we can leverage the faster `.lstrip!`.
- Corrected the gsub regexp to no longer match empty strings (`+` vs `*`).
- Modify the matched strings in place to save on allocations
```ruby
require 'benchmark/ips'
require 'active_support/all'
module ActiveSupport
module Inflector
def humanize2(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
result = lower_case_and_underscored_word.to_s.dup
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
result.tr!("_", " ")
result.lstrip!
unless keep_id_suffix
result.delete_suffix!(" id")
end
result.gsub!(/([a-z\d]+)/i) do |match|
match.downcase!
inflections.acronyms[match] || match
end
if capitalize
result.sub!(/\A\w/) do |match|
match.upcase!
match
end
end
result
end
end
end
%w(foo foo_bar_id ____foo_bar).each do |str|
puts "== Comparing with #{str.inspect} (#{RUBY_VERSION}) =="
unless ActiveSupport::Inflector.humanize(str) == ActiveSupport::Inflector.humanize2(str)
raise "#{ActiveSupport::Inflector.humanize2(str)} != #{ActiveSupport::Inflector.humanize(str)}"
end
Benchmark.ips do |x|
x.report('humanize') { ActiveSupport::Inflector.humanize(str) }
x.report('humanize2') { ActiveSupport::Inflector.humanize2(str) }
x.compare!
end
puts
end
```
```
== Comparing with "foo" (2.7.2) ==
Warming up --------------------------------------
humanize 25.593k i/100ms
humanize2 29.256k i/100ms
Calculating -------------------------------------
humanize 263.989k (± 1.9%) i/s - 1.331M in 5.043110s
humanize2 299.883k (± 2.2%) i/s - 1.521M in 5.075478s
Comparison:
humanize2: 299882.5 i/s
humanize: 263989.1 i/s - 1.14x (± 0.00) slower
== Comparing with "foo_bar_id" (2.7.2) ==
Warming up --------------------------------------
humanize 18.187k i/100ms
humanize2 25.678k i/100ms
Calculating -------------------------------------
humanize 183.702k (± 1.5%) i/s - 927.537k in 5.050326s
humanize2 250.470k (± 2.5%) i/s - 1.258M in 5.026682s
Comparison:
humanize2: 250469.6 i/s
humanize: 183702.3 i/s - 1.36x (± 0.00) slower
== Comparing with "____foo_bar" (2.7.2) ==
Warming up --------------------------------------
humanize 18.577k i/100ms
humanize2 24.686k i/100ms
Calculating -------------------------------------
humanize 188.868k (± 1.5%) i/s - 947.427k in 5.017524s
humanize2 255.650k (± 1.8%) i/s - 1.284M in 5.022833s
Comparison:
humanize2: 255649.8 i/s
```
In the docs [here](https://apidock.com/rails/v6.0.0/ActiveSupport/MessageVerifier) under the `Making messages expire` section, it was a little unclear what is `doowad` & `parcel` which ideally be a +string+ or a variable but in the complete documentation, we haven't used the reference of any variables except +token+, so these 2 should be a string.
As mentioned in
https://github.com/rails/rails/pull/40770#issuecomment-748347066 we
should default to SHA256 where SHA1 is used today. This switches over
the ActiveSupport::Digest to use SHA256 for new applications.
It also updates the constants to always refer to and use the OpenSSL
constants as well, as also discussed in that PR.
This change allows for configuration of the hash digest that is used in
the key generator for key derivation.
SHA1 is an outdated algorithm and security auditors tend to frown on
its usage. By allowing this to be configured, it becomes possible to
move to a more up to date hash mechanism.
While I don't think this has any current relevant security implications,
especially not with a proper random secret base, moving away from SHA1
makes conversations with auditors and FIPS compliance checks easier
since the best answer is always that an approved algorithm is used.
A rotation can be built using this change with an approach like the
following for encrypted cookies:
```ruby
Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
secret_key_base = Rails.application.secrets.secret_key_base
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1)
key_len = ActiveSupport::MessageEncryptor.key_len
secret = key_generator.generate_key(salt, key_len)
cookies.rotate :encrypted, secret
end
```
This turns the default into using SHA256 but also still accepts secrets
derived using SHA1.
The defaults for new apps is here changed to use SHA256. Existing apps
will keep using SHA1.
The Digest::SHA2 class implicitly ends up using SHA256 by default, but
there's no OpenSSL version of the same class (OpenSSL::Digest::SHA2 does
not exist).
To make it easier to force OpenSSL for hash usage and not use deprecated
OpenSSL APIs from Digest on older Ruby versions before 3.0, an approach
like https://github.com/ruby/openssl/pull/377 might be used.
That approach won't work with this SHA2 class though, so it shouldn't be
used. The discussion in https://github.com/rails/rails/pull/40770 also
indicates that in general the intent is to move to OpenSSL constants
anyway, so this makes that move a lot easier as well.
Lastly, this updates the cache key usage in the Redis cache store. This
store seems to be the only one not using the configured
ActiveSupport::Digest class like all other cache usage. Switching it
from the not recommended SHA2 class to the ActiveSupport one makes it
more consistent with the rest. This also updates the cache key name to
not call out the implementation of the hash, and updates memcache in a
similar way (where it was still referring to MD5 which is not guaranteed
to be used for ActiveSupport::Digest).
This test was originally written with the intention of asserting that
a runtime error related to XXE will be raised by the parser. However,
because initial whitespace is present before the doctype,
XmlMini_NokogiriSAX::HashBuilder has been raising an unrelated error
in this test.
Related to #41015
---
Using Nokogiri v1.10.10, the error being raised without this change
is:
> XML declaration allowed only at the start of the document
but with this change we see the expected exception from the libxml2
SAX parser:
> Entity 'a' not defined
Using Nokogiri v1.11.0, in which error handling is broken (see
sparklemotion/nokogiri#2168), without this change we see an exception
being raised by HashBuilder because `characters` is called before
`start_element` and so the content hash isn't initialized (see
The error being raised with this change is:
> Parse stack not empty!
which is not the error we want (because of
sparklemotion/nokogiri#2168), but the test still passes.
Using Nokogiri with the fix from sparklemotion/nokogiri#2169, the
error being raised without this change is:
> XML declaration allowed only at the start of the document
but with this change will be:
> Entity 'a' not defined
and we're back to the expected behavior.
`rescue_from` works for rescuing exceptions in controller actions, but it's not specific to ActionController.
This change updates the docs to clarify the specifics of how `rescue_from` is used and that its use goes beyond controller actions.
Note: I believe the original addition of this documentation was added as part of the move from [`ActionController::Rescue` to `ActiveSupport::Rescuable`](259a7a844b (diff-2276a3674b84e1262c94cc36346f9fbd8d00e0a4542bb991a8846c06d86335b1R12))
The `expires_in` option is easy to misremember or mistype as `expire_in`
or `expired_in`, with potentially harmful results. If a developer wants
to cache a value for only 2 seconds but mistakenly types
`expire_in: 2.seconds`, that value will instead be cached for the
default time, likely 6 hours, meaning that users of the site will see
the same data for much longer than they should, and the only recovery
(short of waiting for the 6 hours to elapse) is to manually expire all
relevant keys. This commit allows cache stores to recognize these common
typos as aliases by normalizing them before consuming the options.
In general, we should be careful about adding too many aliases for
options to the cache stores, since each option key used by the base
Cache::Store class is one fewer key that each cache implementation can
customize for itself. This case was approved because of the similarity
of the aliases to the root key and the potential damage caused by
mistaking them.
Fixes#39850.
While the warning is useful in itself, it doesn't tell you what file is
specifically causing the issue which can make resolving it harder than
it should be. As we've got the path already, we can simply include the
location of the problematic file in the warning message.
The docs for Time#xmlschema note "You must require 'time' to use this
method." -- see
https://ruby-doc.org/stdlib-2.7.2/libdoc/time/rdoc/Time.html#method-i-xmlschema
Apparently in most cases, by the time `core_ext/time/conversions.rb` is
loaded, "time" has already been required, however that is not a
guarantee. If it isn't, you'll get a "NameError (undefined method
`xmlschema' for class `Time')". A simple repro is to launch `irb` and
do:
> require 'active_support'
> require 'active_support/core_ext/date_time'
This can even happen on some systems with just a:
> require 'active_support'
> require 'active_support/core_ext'
That is because `active_support/core_ext.rb` uses `Dir.glob` to
enumerate and then require all ruby files in the `core_ext` directory,
but "the order in which the results are returned [from Dir.glob]
depends on your system" -- see
https://ruby-doc.org/core-2.7.2/Dir.html#method-c-glob
Therefore this commit also sorts those results to make the load order
deterministic and system-agnostic.
`#dup` resets the extended Logger methods that could come from enabling broadcasting. That would mean if we create a tagged logger from a Logger with broadcasting enabled (usually to stdout), the new tagged logger will not perform broadcasting.
Making the fork method private by calling `private :fork` raises a
"no superclass method `fork'" error when calling super in a subclass on
ruby <= 2.5.3. The error doesn't occur on ruby 2.5.4 and higher.
Making the method private by redefining doesn't raise the error.
The possible fix on 2.5.4 is 75aba10d7a
The error can be reproduced with the following script on ruby 2.5.3:
```
class Cluster
def start
fork { puts "forked!" }
end
end
module CoreExt
def fork(*)
super
end
end
module CoreExtPrivate
include CoreExt
private :fork
end
::Object.prepend(CoreExtPrivate)
Cluster.new.start
```
Fixes#40603
Fixes#39976
Prior to this commit it was possible to pass a single argument block to
`ActiveSupport::Notifications.subscribe`, rather than 5 separate
arguments:
```rb
ActiveSupport::Notifications.subscribe('some_event') do |event|
puts "Reacting to #{event.name}"
end
```
But it was not possible to do the same with a lambda, since the lambda
parameter is a required (`:req`) parameter, but we were checking only
for an optional (`:opt`) parameter.
```rb
listener = ->(event) do
puts "Reacting to #{event.name}"
end
ActiveSupport::Notifications.subscribe('some_event', &listener)
```
It was also not possible to do this with a custom callable object, since
the custom callable does not respond directly to `:parameters` (although
it's `:call` method object does).
```rb
class CustomListener
def call(event)
puts "Reacting to #{event.name}"
end
end
ActiveSupport::Notifications.subscribe('some_event', CustomListener.new)
```
Prior to this commit these examples would have raised `ArgumentError:
wrong number of arguments (given 5, expected 1)`.
With this commit the single argument lambda and custom callable work
like the single argument block.
did_you_mean 1.5.0 will add suggestions to `LoadError`. This means that
`LoadError#message` will now return a new string on each invocation, and
mutating the result will no longer modify the error's message.
For example PG refers to https://www.ietf.org/rfc/rfc3339.txt when converting(Ref: https://www.postgresql.org/docs/current/datatype-datetime.html)
According to the ref there is no explicit mention of allowing sign before the parts, which reads as below:
Durations:
dur-second = 1*DIGIT "S"
dur-minute = 1*DIGIT "M" [dur-second]
dur-hour = 1*DIGIT "H" [dur-minute]
dur-time = "T" (dur-hour / dur-minute / dur-second)
dur-day = 1*DIGIT "D"
dur-week = 1*DIGIT "W"
dur-month = 1*DIGIT "M" [dur-day]
dur-year = 1*DIGIT "Y" [dur-month]
dur-date = (dur-day / dur-month / dur-year) [dur-time]
duration = "P" (dur-date / dur-time / dur-week)
We should not attempt to move sign forward in this case.