All Files ( 38.02% covered at 28.31 hits/line )
254 files in total.
9554 relevant lines,
3632 lines covered and
5922 lines missed.
(
38.02%
)
# frozen_string_literal: true
#--
# Copyright (c) 2005-2020 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
- 24
require "securerandom"
- 24
require "active_support/dependencies/autoload"
- 24
require "active_support/version"
- 24
require "active_support/logger"
- 24
require "active_support/lazy_load_hooks"
- 24
require "active_support/core_ext/date_and_time/compatibility"
- 24
module ActiveSupport
- 24
extend ActiveSupport::Autoload
- 24
autoload :Concern
- 24
autoload :ActionableError
- 24
autoload :ConfigurationFile
- 24
autoload :CurrentAttributes
- 24
autoload :Dependencies
- 24
autoload :DescendantsTracker
- 24
autoload :ExecutionWrapper
- 24
autoload :Executor
- 24
autoload :FileUpdateChecker
- 24
autoload :EventedFileUpdateChecker
- 24
autoload :ForkTracker
- 24
autoload :LogSubscriber
- 24
autoload :Notifications
- 24
autoload :Reloader
- 24
autoload :SecureCompareRotator
- 24
eager_autoload do
- 24
autoload :BacktraceCleaner
- 24
autoload :ProxyObject
- 24
autoload :Benchmarkable
- 24
autoload :Cache
- 24
autoload :Callbacks
- 24
autoload :Configurable
- 24
autoload :Deprecation
- 24
autoload :Digest
- 24
autoload :Gzip
- 24
autoload :Inflector
- 24
autoload :JSON
- 24
autoload :KeyGenerator
- 24
autoload :MessageEncryptor
- 24
autoload :MessageVerifier
- 24
autoload :Multibyte
- 24
autoload :NumberHelper
- 24
autoload :OptionMerger
- 24
autoload :OrderedHash
- 24
autoload :OrderedOptions
- 24
autoload :StringInquirer
- 24
autoload :EnvironmentInquirer
- 24
autoload :TaggedLogging
- 24
autoload :XmlMini
- 24
autoload :ArrayInquirer
end
- 24
autoload :Rescuable
- 24
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
- 24
autoload :TestCase
- 24
def self.eager_load!
super
NumberHelper.eager_load!
end
- 24
cattr_accessor :test_order # :nodoc:
- 24
def self.to_time_preserves_timezone
DateAndTime::Compatibility.preserve_timezone
end
- 24
def self.to_time_preserves_timezone=(value)
- 23
DateAndTime::Compatibility.preserve_timezone = value
end
- 24
def self.utc_to_local_returns_utc_offset_times
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times
end
- 24
def self.utc_to_local_returns_utc_offset_times=(value)
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
end
end
- 24
autoload :I18n, "active_support/i18n"
# frozen_string_literal: true
- 1
module ActiveSupport
# Actionable errors let's you define actions to resolve an error.
#
# To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt>
# module and invoke the +action+ class macro to define the action. An action
# needs a name and a block to execute.
- 1
module ActionableError
- 1
extend Concern
- 1
class NonActionable < StandardError; end
- 1
included do
- 1
class_attribute :_actions, default: {}
end
- 1
def self.actions(error) # :nodoc:
case error
when ActionableError, -> it { Class === it && it < ActionableError }
error._actions
else
{}
end
end
- 1
def self.dispatch(error, name) # :nodoc:
actions(error).fetch(name).call
rescue KeyError
raise NonActionable, "Cannot find action \"#{name}\""
end
- 1
module ClassMethods
# Defines an action that can resolve the error.
#
# class PendingMigrationError < MigrationError
# include ActiveSupport::ActionableError
#
# action "Run pending migrations" do
# ActiveRecord::Tasks::DatabaseTasks.migrate
# end
# end
- 1
def action(name, &block)
- 2
_actions[name] = block
end
end
end
end
# frozen_string_literal: true
require "active_support"
require "active_support/time"
require "active_support/core_ext"
# frozen_string_literal: true
- 1
require "active_support/core_ext/symbol/starts_ends_with"
- 1
module ActiveSupport
# Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check
# its string-like contents:
#
# variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
#
# variants.phone? # => true
# variants.tablet? # => true
# variants.desktop? # => false
- 1
class ArrayInquirer < Array
# Passes each element of +candidates+ collection to ArrayInquirer collection.
# The method returns true if any element from the ArrayInquirer collection
# is equal to the stringified or symbolized form of any element in the +candidates+ collection.
#
# If +candidates+ collection is not given, method returns true.
#
# variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
#
# variants.any? # => true
# variants.any?(:phone, :tablet) # => true
# variants.any?('phone', 'desktop') # => true
# variants.any?(:desktop, :watch) # => false
- 1
def any?(*candidates)
if candidates.none?
super
else
candidates.any? do |candidate|
include?(candidate.to_sym) || include?(candidate.to_s)
end
end
end
- 1
private
- 1
def respond_to_missing?(name, include_private = false)
name.end_with?("?") || super
end
- 1
def method_missing(name, *args)
if name.end_with?("?")
any?(name[0..-2])
else
super
end
end
end
end
# frozen_string_literal: true
module ActiveSupport
# Backtraces often include many lines that are not relevant for the context
# under review. This makes it hard to find the signal amongst the backtrace
# noise, and adds debugging time. With a BacktraceCleaner, filters and
# silencers are used to remove the noisy lines, so that only the most relevant
# lines remain.
#
# Filters are used to modify lines of data, while silencers are used to remove
# lines entirely. The typical filter use case is to remove lengthy path
# information from the start of each line, and view file paths relevant to the
# app directory instead of the file system root. The typical silencer use case
# is to exclude the output of a noisy library from the backtrace, so that you
# can focus on the rest.
#
# bc = ActiveSupport::BacktraceCleaner.new
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
# and show as much data as possible, you can always call
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
# backtrace to a pristine state. If you need to reconfigure an existing
# BacktraceCleaner so that it does not filter or modify the paths of any lines
# of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
# These two methods will give you a completely untouched backtrace.
#
# Inspired by the Quiet Backtrace gem by thoughtbot.
class BacktraceCleaner
def initialize
@filters, @silencers = [], []
add_gem_filter
add_gem_silencer
add_stdlib_silencer
end
# Returns the backtrace after all filters and silencers have been run
# against it. Filters run first, then silencers.
def clean(backtrace, kind = :silent)
filtered = filter_backtrace(backtrace)
case kind
when :silent
silence(filtered)
when :noise
noise(filtered)
else
filtered
end
end
alias :filter :clean
# Adds a filter from the block provided. Each line in the backtrace will be
# mapped against this filter.
#
# # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
# backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
def add_filter(&block)
@filters << block
end
# Adds a silencer from the block provided. If the silencer returns +true+
# for a given line, it will be excluded from the clean backtrace.
#
# # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
# backtrace_cleaner.add_silencer { |line| /puma/.match?(line) }
def add_silencer(&block)
@silencers << block
end
# Removes all silencers, but leaves in the filters. Useful if your
# context of debugging suddenly expands as you suspect a bug in one of
# the libraries you use.
def remove_silencers!
@silencers = []
end
# Removes all filters, but leaves in the silencers. Useful if you suddenly
# need to see entire filepaths in the backtrace that you had already
# filtered out.
def remove_filters!
@filters = []
end
private
FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) /
def add_gem_filter
gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
return if gems_paths.empty?
gems_regexp = %r{(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)}
gems_result = '\3 (\4) \5'
add_filter { |line| line.sub(gems_regexp, gems_result) }
end
def add_gem_silencer
add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) }
end
def add_stdlib_silencer
add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) }
end
# Process +ary+ via +filters+ using +method+, ensuring
# _something_ gets returned.
def process_collection(ary, filters, method)
filters.reduce(ary) { |bt, f| bt.send(method) { |line| f.call(line) } }
end
# Use @filters to transform the backtrace via map
def filter_backtrace(backtrace)
process_collection backtrace, @filters, :map
end
# Use @silencers to reject parts of the backtrace. Guarantee
# something non-empty is returned.
def silence(backtrace)
result = process_collection backtrace, @silencers, :reject
result.first ? result : backtrace.dup
end
# Use @silencers to select parts of the backtrace. Guarantee
# something non-empty is returned.
def noise(backtrace)
result = backtrace.select { |line| @silencers.any? { |s| s.call(line) } }
result.first ? result : backtrace.dup
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/benchmark"
- 2
require "active_support/core_ext/hash/keys"
- 2
module ActiveSupport
- 2
module Benchmarkable
# Allows you to measure the execution time of a block in a template and
# records the result to the log. Wrap this block around expensive operations
# or possible bottlenecks to get a time reading for the operation. For
# example, let's say you thought your file processing method was taking too
# long; you could wrap it in a benchmark block.
#
# <% benchmark 'Process data files' do %>
# <%= expensive_files_operation %>
# <% end %>
#
# That would add something like "Process data files (345.2ms)" to the log,
# which you can then use to compare timings when optimizing your code.
#
# You may give an optional logger level (<tt>:debug</tt>, <tt>:info</tt>,
# <tt>:warn</tt>, <tt>:error</tt>) as the <tt>:level</tt> option. The
# default logger level value is <tt>:info</tt>.
#
# <% benchmark 'Low-level files', level: :debug do %>
# <%= lowlevel_files_operation %>
# <% end %>
#
# Finally, you can pass true as the third argument to silence all log
# activity (other than the timing information) from inside the block. This
# is great for boiling down a noisy block to just a single statement that
# produces one log line:
#
# <% benchmark 'Process data files', level: :info, silence: true do %>
# <%= expensive_and_chatty_files_operation %>
# <% end %>
- 2
def benchmark(message = "Benchmarking", options = {})
if logger
options.assert_valid_keys(:level, :silence)
options[:level] ||= :info
result = nil
ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }
logger.send(options[:level], "%s (%.1fms)" % [ message, ms ])
result
else
yield
end
end
end
end
# frozen_string_literal: true
- 1
begin
- 1
require "builder"
rescue LoadError => e
$stderr.puts "You don't have builder installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
# frozen_string_literal: true
- 13
require "zlib"
- 13
require "active_support/core_ext/array/extract_options"
- 13
require "active_support/core_ext/array/wrap"
- 13
require "active_support/core_ext/enumerable"
- 13
require "active_support/core_ext/module/attribute_accessors"
- 13
require "active_support/core_ext/numeric/bytes"
- 13
require "active_support/core_ext/numeric/time"
- 13
require "active_support/core_ext/object/to_param"
- 13
require "active_support/core_ext/object/try"
- 13
require "active_support/core_ext/string/inflections"
- 13
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
- 13
module Cache
- 13
autoload :FileStore, "active_support/cache/file_store"
- 13
autoload :MemoryStore, "active_support/cache/memory_store"
- 13
autoload :MemCacheStore, "active_support/cache/mem_cache_store"
- 13
autoload :NullStore, "active_support/cache/null_store"
- 13
autoload :RedisCacheStore, "active_support/cache/redis_cache_store"
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
- 13
UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
- 13
module Strategy
- 13
autoload :LocalCache, "active_support/cache/strategy/local_cache"
end
- 13
class << self
# Creates a new Store object according to the given options.
#
# If no arguments are passed to this method, then a new
# ActiveSupport::Cache::MemoryStore object will be returned.
#
# If you pass a Symbol as the first argument, then a corresponding cache
# store class under the ActiveSupport::Cache namespace will be created.
# For example:
#
# ActiveSupport::Cache.lookup_store(:memory_store)
# # => returns a new ActiveSupport::Cache::MemoryStore object
#
# ActiveSupport::Cache.lookup_store(:mem_cache_store)
# # => returns a new ActiveSupport::Cache::MemCacheStore object
#
# Any additional arguments will be passed to the corresponding cache store
# class's constructor:
#
# ActiveSupport::Cache.lookup_store(:file_store, '/tmp/cache')
# # => same as: ActiveSupport::Cache::FileStore.new('/tmp/cache')
#
# If the first argument is not a Symbol, then it will simply be returned:
#
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
# # => returns MyOwnCacheStore.new
- 13
def lookup_store(store = nil, *parameters)
case store
when Symbol
options = parameters.extract_options!
retrieve_store_class(store).new(*parameters, **options)
when Array
lookup_store(*store)
when nil
ActiveSupport::Cache::MemoryStore.new
else
store
end
end
# Expands out the +key+ argument into a key that can be used for the
# cache store. Optionally accepts a namespace, and all keys will be
# scoped within that namespace.
#
# If the +key+ argument provided is an array, or responds to +to_a+, then
# each of elements in the array will be turned into parameters/keys and
# concatenated into a single key. For example:
#
# ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar"
# ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar"
#
# The +key+ argument can also respond to +cache_key+ or +to_param+.
- 13
def expand_cache_key(key, namespace = nil)
expanded_cache_key = namespace ? +"#{namespace}/" : +""
if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
expanded_cache_key << "#{prefix}/"
end
expanded_cache_key << retrieve_cache_key(key)
expanded_cache_key
end
- 13
private
- 13
def retrieve_cache_key(key)
case
when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version
when key.respond_to?(:cache_key) then key.cache_key
when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
else key.to_param
end.to_s
end
# Obtains the specified cache store class, given the name of the +store+.
# Raises an error when the store class cannot be found.
- 13
def retrieve_store_class(store)
# require_relative cannot be used here because the class might be
# provided by another gem, like redis-activesupport for example.
require "active_support/cache/#{store}"
rescue LoadError => e
raise "Could not find cache store adapter for #{store} (#{e})"
else
ActiveSupport::Cache.const_get(store.to_s.camelize)
end
end
# An abstract cache store class. There are multiple cache store
# implementations, each having its own additional features. See the classes
# under the ActiveSupport::Cache module, e.g.
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
# popular cache store for large production websites.
#
# Some implementations may not support all methods beyond the basic cache
# methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
#
# ActiveSupport::Cache::Store can store any serializable Ruby object.
#
# cache = ActiveSupport::Cache::MemoryStore.new
#
# cache.read('city') # => nil
# cache.write('city', "Duckburgh")
# cache.read('city') # => "Duckburgh"
#
# Keys are always translated into Strings and are case sensitive. When an
# object is specified as a key and has a +cache_key+ method defined, this
# method will be called to define the key. Otherwise, the +to_param+
# method will be called. Hashes and Arrays can also be used as keys. The
# elements will be delimited by slashes, and the elements within a Hash
# will be sorted by key so they are consistent.
#
# cache.read('city') == cache.read(:city) # => true
#
# Nil values can be cached.
#
# If your cache is on a shared infrastructure, you can define a namespace
# for your cache entries. If a namespace is defined, it will be prefixed on
# to every key. The namespace can be either a static value or a Proc. If it
# is a Proc, it will be invoked when each key is evaluated so that you can
# use application logic to invalidate keys.
#
# cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
#
# Cached data larger than 1kB are compressed by default. To turn off
# compression, pass <tt>compress: false</tt> to the initializer or to
# individual +fetch+ or +write+ method calls. The 1kB compression
# threshold is configurable with the <tt>:compress_threshold</tt> option,
# specified in bytes.
- 13
class Store
- 13
cattr_accessor :logger, instance_writer: true
- 13
attr_reader :silence, :options
- 13
alias :silence? :silence
- 13
class << self
- 13
private
- 13
def retrieve_pool_options(options)
{}.tap do |pool_options|
pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
end
end
- 13
def ensure_connection_pool_added!
require "connection_pool"
rescue LoadError => e
$stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
end
# Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache.
- 13
def initialize(options = nil)
@options = options ? options.dup : {}
end
# Silences the logger.
- 13
def silence!
@silence = true
self
end
# Silences the logger within a block.
- 13
def mute
previous_silence, @silence = defined?(@silence) && @silence, true
yield
ensure
@silence = previous_silence
end
# Fetches data from the cache, using the given key. If there is data in
# the cache with the given key, then that data is returned.
#
# If there is no such data in the cache (a cache miss), then +nil+ will be
# returned. However, if a block has been passed, that block will be passed
# the key and executed in the event of a cache miss. The return value of the
# block will be written to the cache under the given cache key, and that
# return value will be returned.
#
# cache.write('today', 'Monday')
# cache.fetch('today') # => "Monday"
#
# cache.fetch('city') # => nil
# cache.fetch('city') do
# 'Duckburgh'
# end
# cache.fetch('city') # => "Duckburgh"
#
# You may also specify additional options via the +options+ argument.
# Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
# the cache value as missing even if it's present. Passing a block is
# required when +force+ is true so this always results in a cache write.
#
# cache.write('today', 'Monday')
# cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
# cache.fetch('today', force: true) # => ArgumentError
#
# The +:force+ option is useful when you're calling some other method to
# ask whether you should force a cache write. Otherwise, it's clearer to
# just call <tt>Cache#write</tt>.
#
# Setting <tt>skip_nil: true</tt> will not cache nil result:
#
# cache.fetch('foo') { nil }
# cache.fetch('bar', skip_nil: true) { nil }
# cache.exist?('foo') # => true
# cache.exist?('bar') # => false
#
#
# Setting <tt>compress: false</tt> disables compression of the cache entry.
#
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
# All caches support auto-expiring content after a specified number of
# seconds. This value can be specified as an option to the constructor
# (in which case all entries will be affected), or it can be supplied to
# the +fetch+ or +write+ method to effect just one entry.
#
# cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
# cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
#
# Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
# is of the same version. nil is returned on mismatches despite contents.
# This feature is used to support recyclable cache keys.
#
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
# a cache entry is used very frequently and is under heavy load. If a
# cache expires and due to heavy load several different processes will try
# to read data natively and then they all will try to write to cache. To
# avoid that case the first process to find an expired cache entry will
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
# Yes, this process is extending the time for a stale value by another few
# seconds. Because of extended life of the previous cache, other processes
# will continue to use slightly stale data for a just a bit longer. In the
# meantime that first process will go ahead and will write into cache the
# new value. After that all the processes will start getting the new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
#
# If the process regenerating the entry errors out, the entry will be
# regenerated after the specified number of seconds. Also note that the
# life of stale cache is extended only if it expired recently. Otherwise
# a new value is generated and <tt>:race_condition_ttl</tt> does not play
# any role.
#
# # Set all values to expire after one minute.
# cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
#
# cache.write('foo', 'original value')
# val_1 = nil
# val_2 = nil
# sleep 60
#
# Thread.new do
# val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
# sleep 1
# 'new value 1'
# end
# end
#
# Thread.new do
# val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
# 'new value 2'
# end
# end
#
# cache.fetch('foo') # => "original value"
# sleep 10 # First thread extended the life of cache by another 10 seconds
# cache.fetch('foo') # => "new value 1"
# val_1 # => "new value 1"
# val_2 # => "original value"
#
# Other options will be handled by the specific cache store implementation.
# Internally, #fetch calls #read_entry, and calls #write_entry on a cache
# miss. +options+ will be passed to the #read and #write calls.
#
# For example, MemCacheStore's #write method supports the +:raw+
# option, which tells the memcached server to store all values as strings.
# We can use this option with #fetch too:
#
# cache = ActiveSupport::Cache::MemCacheStore.new
# cache.fetch("foo", force: true, raw: true) do
# :bar
# end
# cache.fetch('foo') # => "bar"
- 13
def fetch(name, options = nil, &block)
if block_given?
options = merged_options(options)
key = normalize_key(name, options)
entry = nil
instrument(:read, name, options) do |payload|
cached_entry = read_entry(key, **options) unless options[:force]
entry = handle_expired_entry(cached_entry, key, options)
entry = nil if entry && entry.mismatched?(normalize_version(name, options))
payload[:super_operation] = :fetch if payload
payload[:hit] = !!entry if payload
end
if entry
get_entry_value(entry, name, options)
else
save_block_result_to_cache(name, options, &block)
end
elsif options && options[:force]
raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
else
read(name, options)
end
end
# Reads data from the cache, using the given key. If there is data in
# the cache with the given key, then that data is returned. Otherwise,
# +nil+ is returned.
#
# Note, if data was written with the <tt>:expires_in</tt> or
# <tt>:version</tt> options, both of these conditions are applied before
# the data is returned.
#
# Options are passed to the underlying cache implementation.
- 13
def read(name, options = nil)
options = merged_options(options)
key = normalize_key(name, options)
version = normalize_version(name, options)
instrument(:read, name, options) do |payload|
entry = read_entry(key, **options)
if entry
if entry.expired?
delete_entry(key, **options)
payload[:hit] = false if payload
nil
elsif entry.mismatched?(version)
payload[:hit] = false if payload
nil
else
payload[:hit] = true if payload
entry.value
end
else
payload[:hit] = false if payload
nil
end
end
end
# Reads multiple values at once from the cache. Options can be passed
# in the last argument.
#
# Some cache implementation may optimize this method.
#
# Returns a hash mapping the names provided to the values found.
- 13
def read_multi(*names)
options = names.extract_options!
options = merged_options(options)
instrument :read_multi, names, options do |payload|
read_multi_entries(names, **options).tap do |results|
payload[:hits] = results.keys
end
end
end
# Cache Storage API to write multiple values at once.
- 13
def write_multi(hash, options = nil)
options = merged_options(options)
instrument :write_multi, hash, options do |payload|
entries = hash.each_with_object({}) do |(name, value), memo|
memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
end
write_multi_entries entries, **options
end
end
# Fetches data from the cache, using the given keys. If there is data in
# the cache with the given keys, then that data is returned. Otherwise,
# the supplied block is called for each key for which there was no data,
# and the result will be written to the cache and returned.
# Therefore, you need to pass a block that returns the data to be written
# to the cache. If you do not want to write the cache when the cache is
# not found, use #read_multi.
#
# Returns a hash with the data for each of the names. For example:
#
# cache.write("bim", "bam")
# cache.fetch_multi("bim", "unknown_key") do |key|
# "Fallback value for key: #{key}"
# end
# # => { "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
- 13
def fetch_multi(*names)
raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
options = names.extract_options!
options = merged_options(options)
instrument :read_multi, names, options do |payload|
reads = read_multi_entries(names, **options)
writes = {}
ordered = names.index_with do |name|
reads.fetch(name) { writes[name] = yield(name) }
end
payload[:hits] = reads.keys
payload[:super_operation] = :fetch_multi
write_multi(writes, options)
ordered
end
end
# Writes the value to the cache, with the key.
#
# Options are passed to the underlying cache implementation.
- 13
def write(name, value, options = nil)
options = merged_options(options)
instrument(:write, name, options) do
entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
write_entry(normalize_key(name, options), entry, **options)
end
end
# Deletes an entry in the cache. Returns +true+ if an entry is deleted.
#
# Options are passed to the underlying cache implementation.
- 13
def delete(name, options = nil)
options = merged_options(options)
instrument(:delete, name) do
delete_entry(normalize_key(name, options), **options)
end
end
# Deletes multiple entries in the cache.
#
# Options are passed to the underlying cache implementation.
- 13
def delete_multi(names, options = nil)
options = merged_options(options)
names.map! { |key| normalize_key(key, options) }
instrument :delete_multi, names do
delete_multi_entries(names, **options)
end
end
# Returns +true+ if the cache contains an entry for the given key.
#
# Options are passed to the underlying cache implementation.
- 13
def exist?(name, options = nil)
options = merged_options(options)
instrument(:exist?, name) do
entry = read_entry(normalize_key(name, options), **options)
(entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
end
end
# Deletes all entries with keys matching the pattern.
#
# Options are passed to the underlying cache implementation.
#
# Some implementations may not support this method.
- 13
def delete_matched(matcher, options = nil)
raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
end
# Increments an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
# Some implementations may not support this method.
- 13
def increment(name, amount = 1, options = nil)
raise NotImplementedError.new("#{self.class.name} does not support increment")
end
# Decrements an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
# Some implementations may not support this method.
- 13
def decrement(name, amount = 1, options = nil)
raise NotImplementedError.new("#{self.class.name} does not support decrement")
end
# Cleanups the cache by removing expired entries.
#
# Options are passed to the underlying cache implementation.
#
# Some implementations may not support this method.
- 13
def cleanup(options = nil)
raise NotImplementedError.new("#{self.class.name} does not support cleanup")
end
# Clears the entire cache. Be careful with this method since it could
# affect other processes if shared cache is being used.
#
# The options hash is passed to the underlying cache implementation.
#
# Some implementations may not support this method.
- 13
def clear(options = nil)
raise NotImplementedError.new("#{self.class.name} does not support clear")
end
- 13
private
# Adds the namespace defined in the options to a pattern designed to
# match keys. Implementations that support delete_matched should call
# this method to translate a pattern that matches names into one that
# matches namespaced keys.
- 13
def key_matcher(pattern, options) # :doc:
prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
if prefix
source = pattern.source
if source.start_with?("^")
source = source[1, source.length]
else
source = ".*#{source[0, source.length]}"
end
Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
else
pattern
end
end
# Reads an entry from the cache implementation. Subclasses must implement
# this method.
- 13
def read_entry(key, **options)
raise NotImplementedError.new
end
# Writes an entry to the cache implementation. Subclasses must implement
# this method.
- 13
def write_entry(key, entry, **options)
raise NotImplementedError.new
end
# Reads multiple entries from the cache implementation. Subclasses MAY
# implement this method.
- 13
def read_multi_entries(names, **options)
names.each_with_object({}) do |name, results|
key = normalize_key(name, options)
entry = read_entry(key, **options)
next unless entry
version = normalize_version(name, options)
if entry.expired?
delete_entry(key, **options)
elsif !entry.mismatched?(version)
results[name] = entry.value
end
end
end
# Writes multiple entries to the cache implementation. Subclasses MAY
# implement this method.
- 13
def write_multi_entries(hash, **options)
hash.each do |key, entry|
write_entry key, entry, **options
end
end
# Deletes an entry from the cache implementation. Subclasses must
# implement this method.
- 13
def delete_entry(key, **options)
raise NotImplementedError.new
end
# Deletes multiples entries in the cache implementation. Subclasses MAY
# implement this method.
- 13
def delete_multi_entries(entries, **options)
entries.inject(0) do |sum, key|
if delete_entry(key, **options)
sum + 1
else
sum
end
end
end
# Merges the default options with ones specific to a method call.
- 13
def merged_options(call_options)
if call_options
if options.empty?
call_options
else
options.merge(call_options)
end
else
options
end
end
# Expands and namespaces the cache key. May be overridden by
# cache stores to do additional normalization.
- 13
def normalize_key(key, options = nil)
namespace_key expanded_key(key), options
end
# Prefix the key with a namespace string:
#
# namespace_key 'foo', namespace: 'cache'
# # => 'cache:foo'
#
# With a namespace block:
#
# namespace_key 'foo', namespace: -> { 'cache' }
# # => 'cache:foo'
- 13
def namespace_key(key, options = nil)
options = merged_options(options)
namespace = options[:namespace]
if namespace.respond_to?(:call)
namespace = namespace.call
end
if key && key.encoding != Encoding::UTF_8
key = key.dup.force_encoding(Encoding::UTF_8)
end
if namespace
"#{namespace}:#{key}"
else
key
end
end
# Expands key to be a consistent string value. Invokes +cache_key+ if
# object responds to +cache_key+. Otherwise, +to_param+ method will be
# called. If the key is a Hash, then keys will be sorted alphabetically.
- 13
def expanded_key(key)
return key.cache_key.to_s if key.respond_to?(:cache_key)
case key
when Array
if key.size > 1
key.collect { |element| expanded_key(element) }
else
expanded_key(key.first)
end
when Hash
key.collect { |k, v| "#{k}=#{v}" }.sort
else
key
end.to_param
end
- 13
def normalize_version(key, options = nil)
(options && options[:version].try(:to_param)) || expanded_version(key)
end
- 13
def expanded_version(key)
case
when key.respond_to?(:cache_version) then key.cache_version.to_param
when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param
when key.respond_to?(:to_a) then expanded_version(key.to_a)
end
end
- 13
def instrument(operation, key, options = nil)
if logger && logger.debug? && !silence?
logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
end
payload = { key: key }
payload.merge!(options) if options.is_a?(Hash)
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
end
- 13
def handle_expired_entry(entry, key, options)
if entry && entry.expired?
race_ttl = options[:race_condition_ttl].to_i
if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
# When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
# for a brief period while the entry is being recalculated.
entry.expires_at = Time.now + race_ttl
write_entry(key, entry, expires_in: race_ttl * 2)
else
delete_entry(key, **options)
end
entry = nil
end
entry
end
- 13
def get_entry_value(entry, name, options)
instrument(:fetch_hit, name, options) { }
entry.value
end
- 13
def save_block_result_to_cache(name, options)
result = instrument(:generate, name, options) do
yield(name)
end
write(name, result, options) unless result.nil? && options[:skip_nil]
result
end
end
# This class is used to represent cache entries. Cache entries have a value, an optional
# expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
# on the cache. The version is used to support the :version option on the cache for rejecting
# mismatches.
#
# Since cache entries in most instances will be serialized, the internals of this class are highly optimized
# using short instance variable names that are lazily defined.
- 13
class Entry # :nodoc:
- 13
attr_reader :version
- 13
DEFAULT_COMPRESS_LIMIT = 1.kilobyte
# Creates a new cache entry for the specified value. Options supported are
# +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
- 13
def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
@value = value
@version = version
@created_at = Time.now.to_f
@expires_in = expires_in && expires_in.to_f
compress!(compress_threshold) if compress
end
- 13
def value
compressed? ? uncompress(@value) : @value
end
- 13
def mismatched?(version)
@version && version && @version != version
end
# Checks if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
- 13
def expired?
@expires_in && @created_at + @expires_in <= Time.now.to_f
end
- 13
def expires_at
@expires_in ? @created_at + @expires_in : nil
end
- 13
def expires_at=(value)
if value
@expires_in = value.to_f - @created_at
else
@expires_in = nil
end
end
# Returns the size of the cached value. This could be less than
# <tt>value.size</tt> if the data is compressed.
- 13
def size
case value
when NilClass
0
when String
@value.bytesize
else
@s ||= Marshal.dump(@value).bytesize
end
end
# Duplicates the value in a class. This is used by cache implementations that don't natively
# serialize entries to protect against accidental cache modifications.
- 13
def dup_value!
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
if @value.is_a?(String)
@value = @value.dup
else
@value = Marshal.load(Marshal.dump(@value))
end
end
end
- 13
private
- 13
def compress!(compress_threshold)
case @value
when nil, true, false, Numeric
uncompressed_size = 0
when String
uncompressed_size = @value.bytesize
else
serialized = Marshal.dump(@value)
uncompressed_size = serialized.bytesize
end
if uncompressed_size >= compress_threshold
serialized ||= Marshal.dump(@value)
compressed = Zlib::Deflate.deflate(serialized)
if compressed.bytesize < uncompressed_size
@value = compressed
@compressed = true
end
end
end
- 13
def compressed?
defined?(@compressed)
end
- 13
def uncompress(value)
Marshal.load(Zlib::Inflate.inflate(value))
end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/marshal"
require "active_support/core_ext/file/atomic"
require "active_support/core_ext/string/conversions"
require "uri/common"
module ActiveSupport
module Cache
# A cache store implementation which stores everything on the filesystem.
#
# FileStore implements the Strategy::LocalCache strategy which implements
# an in-memory cache inside of a block.
class FileStore < Store
prepend Strategy::LocalCache
attr_reader :cache_path
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 226 # max filename size on file system is 255, minus room for timestamp, pid, and random characters appended by Tempfile (used by atomic write)
FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
GITKEEP_FILES = [".gitkeep", ".keep"].freeze
def initialize(cache_path, options = nil)
super(options)
@cache_path = cache_path.to_s
end
# Advertise cache versioning support.
def self.supports_cache_versioning?
true
end
# Deletes all items from the cache. In this case it deletes all the entries in the specified
# file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
# config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
root_dirs = (Dir.children(cache_path) - GITKEEP_FILES)
FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
rescue Errno::ENOENT, Errno::ENOTEMPTY
end
# Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
search_dir(cache_path) do |fname|
entry = read_entry(fname, **options)
delete_entry(fname, **options) if entry && entry.expired?
end
end
# Increments an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
def increment(name, amount = 1, options = nil)
modify_value(name, amount, options)
end
# Decrements an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
def decrement(name, amount = 1, options = nil)
modify_value(name, -amount, options)
end
def delete_matched(matcher, options = nil)
options = merged_options(options)
instrument(:delete_matched, matcher.inspect) do
matcher = key_matcher(matcher, options)
search_dir(cache_path) do |path|
key = file_path_key(path)
delete_entry(path, **options) if key.match(matcher)
end
end
end
private
def read_entry(key, **options)
if File.exist?(key)
entry = File.open(key) { |f| Marshal.load(f) }
entry if entry.is_a?(Cache::Entry)
end
rescue => e
logger.error("FileStoreError (#{e}): #{e.message}") if logger
nil
end
def write_entry(key, entry, **options)
return false if options[:unless_exist] && File.exist?(key)
ensure_cache_path(File.dirname(key))
File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
true
end
def delete_entry(key, **options)
if File.exist?(key)
begin
File.delete(key)
delete_empty_directories(File.dirname(key))
true
rescue => e
# Just in case the error was caused by another process deleting the file first.
raise e if File.exist?(key)
false
end
end
end
# Lock a file for a block so only one process can modify it at a time.
def lock_file(file_name, &block)
if File.exist?(file_name)
File.open(file_name, "r+") do |f|
f.flock File::LOCK_EX
yield
ensure
f.flock File::LOCK_UN
end
else
yield
end
end
# Translate a key into a file path.
def normalize_key(key, options)
key = super
fname = URI.encode_www_form_component(key)
if fname.size > FILEPATH_MAX_SIZE
fname = ActiveSupport::Digest.hexdigest(key)
end
hash = Zlib.adler32(fname)
hash, dir_1 = hash.divmod(0x1000)
dir_2 = hash.modulo(0x1000)
# Make sure file name doesn't exceed file system limits.
if fname.length < FILENAME_MAX_SIZE
fname_paths = fname
else
fname_paths = []
begin
fname_paths << fname[0, FILENAME_MAX_SIZE]
fname = fname[FILENAME_MAX_SIZE..-1]
end until fname.blank?
end
File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths)
end
# Translate a file path into a key.
def file_path_key(path)
fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last
URI.decode_www_form_component(fname, Encoding::UTF_8)
end
# Delete empty directories in the cache.
def delete_empty_directories(dir)
return if File.realpath(dir) == File.realpath(cache_path)
if Dir.children(dir).empty?
Dir.delete(dir) rescue nil
delete_empty_directories(File.dirname(dir))
end
end
# Make sure a file path's directories exist.
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exist?(path)
end
def search_dir(dir, &callback)
return if !File.exist?(dir)
Dir.each_child(dir) do |d|
name = File.join(dir, d)
if File.directory?(name)
search_dir(name, &callback)
else
callback.call name
end
end
end
# Modifies the amount of an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
def modify_value(name, amount, options)
file_name = normalize_key(name, options)
lock_file(file_name) do
options = merged_options(options)
if num = read(name, options)
num = num.to_i + amount
write(name, num, options)
num
end
end
end
end
end
end
# frozen_string_literal: true
begin
require "dalli"
rescue LoadError => e
$stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
require "active_support/core_ext/enumerable"
require "active_support/core_ext/marshal"
require "active_support/core_ext/array/extract_options"
module ActiveSupport
module Cache
# A cache store implementation which stores data in Memcached:
# https://memcached.org
#
# This is currently the most popular cache store for production websites.
#
# Special features:
# - Clustering and load balancing. One can specify multiple memcached servers,
# and MemCacheStore will load balance between all available servers. If a
# server goes down, then MemCacheStore will ignore it until it comes back up.
#
# MemCacheStore implements the Strategy::LocalCache strategy which implements
# an in-memory cache inside of a block.
class MemCacheStore < Store
# Provide support for raw values in the local cache strategy.
module LocalCacheWithRaw # :nodoc:
private
def write_entry(key, entry, **options)
if options[:raw] && local_cache
raw_entry = Entry.new(entry.value.to_s)
raw_entry.expires_at = entry.expires_at
super(key, raw_entry, **options)
else
super
end
end
end
# Advertise cache versioning support.
def self.supports_cache_versioning?
true
end
prepend Strategy::LocalCache
prepend LocalCacheWithRaw
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
# Creates a new Dalli::Client instance with specified addresses and options.
# By default address is equal localhost:11211.
#
# ActiveSupport::Cache::MemCacheStore.build_mem_cache
# # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil>
# ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290')
# # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil>
def self.build_mem_cache(*addresses) # :nodoc:
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty?
pool_options = retrieve_pool_options(options)
if pool_options.empty?
Dalli::Client.new(addresses, options)
else
ensure_connection_pool_added!
ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
end
end
# Creates a new MemCacheStore object, with the given memcached server
# addresses. Each address is either a host name, or a host-with-port string
# in the form of "host_name:port". For example:
#
# ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
#
# If no addresses are specified, then MemCacheStore will connect to
# localhost port 11211 (the default memcached port).
def initialize(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
super(options)
unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
end
if addresses.first.is_a?(Dalli::Client)
@data = addresses.first
else
mem_cache_options = options.dup
UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
@data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
end
end
# Increment a cached value. This method uses the memcached incr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
def increment(name, amount = 1, options = nil)
options = merged_options(options)
instrument(:increment, name, amount: amount) do
rescue_error_with nil do
@data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
# Decrement a cached value. This method uses the memcached decr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
def decrement(name, amount = 1, options = nil)
options = merged_options(options)
instrument(:decrement, name, amount: amount) do
rescue_error_with nil do
@data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
# Clear the entire cache on all memcached servers. This method should
# be used with care when shared cache is being used.
def clear(options = nil)
rescue_error_with(nil) { @data.with { |c| c.flush_all } }
end
# Get the statistics from the memcached servers.
def stats
@data.with { |c| c.stats }
end
private
# Read an entry from the cache.
def read_entry(key, **options)
rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) }
end
# Write an entry to the cache.
def write_entry(key, entry, **options)
method = options && options[:unless_exist] ? :add : :set
value = options[:raw] ? entry.value.to_s : entry
expires_in = options[:expires_in].to_i
if expires_in > 0 && !options[:raw]
# Set the memcache expire a few minutes in the future to support race condition ttls on read
expires_in += 5.minutes
end
rescue_error_with false do
# The value "compress: false" prevents duplicate compression within Dalli.
@data.with { |c| c.send(method, key, value, expires_in, **options, compress: false) }
end
end
# Reads multiple entries from the cache implementation.
def read_multi_entries(names, **options)
keys_to_names = names.index_by { |name| normalize_key(name, options) }
raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
values = {}
raw_values.each do |key, value|
entry = deserialize_entry(value)
unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
values[keys_to_names[key]] = entry.value
end
end
values
end
# Delete an entry from the cache.
def delete_entry(key, **options)
rescue_error_with(false) { @data.with { |c| c.delete(key) } }
end
# Memcache keys are binaries. So we need to force their encoding to binary
# before applying the regular expression to ensure we are escaping all
# characters properly.
def normalize_key(key, options)
key = super.dup
key = key.force_encoding(Encoding::ASCII_8BIT)
key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250
key
end
def deserialize_entry(entry)
if entry
entry.is_a?(Entry) ? entry : Entry.new(entry, compress: false)
end
end
def rescue_error_with(fallback)
yield
rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
fallback
end
end
end
end
# frozen_string_literal: true
require "monitor"
module ActiveSupport
module Cache
# A cache store implementation which stores everything into memory in the
# same process. If you're running multiple Ruby on Rails server processes
# (which is the case if you're using Phusion Passenger or puma clustered mode),
# then this means that Rails server process instances won't be able
# to share cache data with each other and this may not be the most
# appropriate cache in that scenario.
#
# This cache has a bounded size specified by the :size options to the
# initializer (default is 32Mb). When the cache exceeds the allotted size,
# a cleanup will occur which tries to prune the cache down to three quarters
# of the maximum size by removing the least recently used entries.
#
# MemoryStore is thread-safe.
class MemoryStore < Store
def initialize(options = nil)
options ||= {}
super(options)
@data = {}
@max_size = options[:size] || 32.megabytes
@max_prune_time = options[:max_prune_time] || 2
@cache_size = 0
@monitor = Monitor.new
@pruning = false
end
# Advertise cache versioning support.
def self.supports_cache_versioning?
true
end
# Delete all data stored in a given cache store.
def clear(options = nil)
synchronize do
@data.clear
@cache_size = 0
end
end
# Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
instrument(:cleanup, size: @data.size) do
keys = synchronize { @data.keys }
keys.each do |key|
entry = @data[key]
delete_entry(key, **options) if entry && entry.expired?
end
end
end
# To ensure entries fit within the specified memory prune the cache by removing the least
# recently accessed entries.
def prune(target_size, max_time = nil)
return if pruning?
@pruning = true
begin
start_time = Concurrent.monotonic_time
cleanup
instrument(:prune, target_size, from: @cache_size) do
keys = synchronize { @data.keys }
keys.each do |key|
delete_entry(key, **options)
return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
end
end
ensure
@pruning = false
end
end
# Returns true if the cache is currently being pruned.
def pruning?
@pruning
end
# Increment an integer value in the cache.
def increment(name, amount = 1, options = nil)
modify_value(name, amount, options)
end
# Decrement an integer value in the cache.
def decrement(name, amount = 1, options = nil)
modify_value(name, -amount, options)
end
# Deletes cache entries if the cache key matches a given pattern.
def delete_matched(matcher, options = nil)
options = merged_options(options)
instrument(:delete_matched, matcher.inspect) do
matcher = key_matcher(matcher, options)
keys = synchronize { @data.keys }
keys.each do |key|
delete_entry(key, **options) if key.match(matcher)
end
end
end
def inspect # :nodoc:
"#<#{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
end
# Synchronize calls to the cache. This should be called wherever the underlying cache implementation
# is not thread safe.
def synchronize(&block) # :nodoc:
@monitor.synchronize(&block)
end
private
PER_ENTRY_OVERHEAD = 240
def cached_size(key, entry)
key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
end
def read_entry(key, **options)
entry = nil
synchronize do
entry = @data.delete(key)
if entry
@data[key] = entry
entry = entry.dup
end
end
entry&.dup_value!
entry
end
def write_entry(key, entry, **options)
entry.dup_value!
synchronize do
return false if options[:unless_exist] && @data.key?(key)
old_entry = @data.delete(key)
if old_entry
@cache_size -= (old_entry.size - entry.size)
else
@cache_size += cached_size(key, entry)
end
@data[key] = entry
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
true
end
end
def delete_entry(key, **options)
synchronize do
entry = @data.delete(key)
@cache_size -= cached_size(key, entry) if entry
!!entry
end
end
def modify_value(name, amount, options)
options = merged_options(options)
synchronize do
if num = read(name, options)
num = num.to_i + amount
write(name, num, options)
num
end
end
end
end
end
end
# frozen_string_literal: true
module ActiveSupport
module Cache
# A cache store implementation which doesn't actually store anything. Useful in
# development and test environments where you don't want caching turned on but
# need to go through the caching interface.
#
# This cache does implement the local cache strategy, so values will actually
# be cached inside blocks that utilize this strategy. See
# ActiveSupport::Cache::Strategy::LocalCache for more details.
class NullStore < Store
prepend Strategy::LocalCache
# Advertise cache versioning support.
def self.supports_cache_versioning?
true
end
def clear(options = nil)
end
def cleanup(options = nil)
end
def increment(name, amount = 1, options = nil)
end
def decrement(name, amount = 1, options = nil)
end
def delete_matched(matcher, options = nil)
end
private
def read_entry(key, **options)
end
def write_entry(key, entry, **options)
true
end
def delete_entry(key, **options)
false
end
end
end
end
# frozen_string_literal: true
- 3
begin
- 3
gem "redis", ">= 4.0.1"
- 3
require "redis"
- 3
require "redis/distributed"
rescue LoadError
warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
raise
end
# Prefer the hiredis driver but don't require it.
- 3
begin
- 3
require "redis/connection/hiredis"
rescue LoadError
end
- 3
require "digest/sha2"
- 3
require "active_support/core_ext/marshal"
- 3
module ActiveSupport
- 3
module Cache
- 3
module ConnectionPoolLike
- 3
def with
yield self
end
end
- 3
::Redis.include(ConnectionPoolLike)
- 3
::Redis::Distributed.include(ConnectionPoolLike)
# Redis cache store.
#
# Deployment note: Take care to use a *dedicated Redis cache* rather
# than pointing this at your existing Redis server. It won't cope well
# with mixed usage patterns and it won't expire cache entries by default.
#
# Redis cache server setup guide: https://redis.io/topics/lru-cache
#
# * Supports vanilla Redis, hiredis, and Redis::Distributed.
# * Supports Memcached-like sharding across Redises with Redis::Distributed.
# * Fault tolerant. If the Redis server is unavailable, no exceptions are
# raised. Cache fetches are all misses and writes are dropped.
# * Local cache. Hot in-memory primary cache within block/middleware scope.
# * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
# 4.0.1+ for distributed mget support.
# * +delete_matched+ support for Redis KEYS globs.
- 3
class RedisCacheStore < Store
# Keys are truncated with their own SHA2 digest if they exceed 1kB
- 3
MAX_KEY_BYTESIZE = 1024
- 3
DEFAULT_REDIS_OPTIONS = {
connect_timeout: 20,
read_timeout: 1,
write_timeout: 1,
reconnect_attempts: 0,
}
- 3
DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
if logger
logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
end
end
# The maximum number of entries to receive per SCAN call.
- 3
SCAN_BATCH_SIZE = 1000
- 3
private_constant :SCAN_BATCH_SIZE
# Advertise cache versioning support.
- 3
def self.supports_cache_versioning?
true
end
# Support raw values in the local cache strategy.
- 3
module LocalCacheWithRaw # :nodoc:
- 3
private
- 3
def write_entry(key, entry, **options)
if options[:raw] && local_cache
raw_entry = Entry.new(serialize_entry(entry, raw: true))
raw_entry.expires_at = entry.expires_at
super(key, raw_entry, **options)
else
super
end
end
- 3
def write_multi_entries(entries, **options)
if options[:raw] && local_cache
raw_entries = entries.map do |key, entry|
raw_entry = Entry.new(serialize_entry(entry, raw: true))
raw_entry.expires_at = entry.expires_at
end.to_h
super(raw_entries, **options)
else
super
end
end
end
- 3
prepend Strategy::LocalCache
- 3
prepend LocalCacheWithRaw
- 3
class << self
# Factory method to create a new Redis instance.
#
# Handles four options: :redis block, :redis instance, single :url
# string, and multiple :url strings.
#
# Option Class Result
# :redis Proc -> options[:redis].call
# :redis Object -> options[:redis]
# :url String -> Redis.new(url: …)
# :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
#
- 3
def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
urls = Array(url)
if redis.is_a?(Proc)
redis.call
elsif redis
redis
elsif urls.size > 1
build_redis_distributed_client urls: urls, **redis_options
else
build_redis_client url: urls.first, **redis_options
end
end
- 3
private
- 3
def build_redis_distributed_client(urls:, **redis_options)
::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
urls.each { |u| dist.add_node url: u }
end
end
- 3
def build_redis_client(url:, **redis_options)
::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
end
end
- 3
attr_reader :redis_options
- 3
attr_reader :max_key_bytesize
# Creates a new Redis cache store.
#
# Handles four options: :redis block, :redis instance, single :url
# string, and multiple :url strings.
#
# Option Class Result
# :redis Proc -> options[:redis].call
# :redis Object -> options[:redis]
# :url String -> Redis.new(url: …)
# :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
#
# No namespace is set by default. Provide one if the Redis cache
# server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
#
# Compression is enabled by default with a 1kB threshold, so cached
# values larger than 1kB are automatically compressed. Disable by
# passing <tt>compress: false</tt> or change the threshold by passing
# <tt>compress_threshold: 4.kilobytes</tt>.
#
# No expiry is set on cache entries by default. Redis is expected to
# be configured with an eviction policy that automatically deletes
# least-recently or -frequently used keys when it reaches max memory.
# See https://redis.io/topics/lru-cache for cache server setup.
#
# Race condition TTL is not set by default. This can be used to avoid
# "thundering herd" cache writes when hot cache entries are expired.
# See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
- 3
def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
@redis_options = redis_options
@max_key_bytesize = MAX_KEY_BYTESIZE
@error_handler = error_handler
super namespace: namespace,
compress: compress, compress_threshold: compress_threshold,
expires_in: expires_in, race_condition_ttl: race_condition_ttl
end
- 3
def redis
@redis ||= begin
pool_options = self.class.send(:retrieve_pool_options, redis_options)
if pool_options.any?
self.class.send(:ensure_connection_pool_added!)
::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
else
self.class.build_redis(**redis_options)
end
end
end
- 3
def inspect
instance = @redis || @redis_options
"#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
end
# Cache Store API implementation.
#
# Read multiple values at once. Returns a hash of requested keys ->
# fetched values.
- 3
def read_multi(*names)
if mget_capable?
instrument(:read_multi, names, options) do |payload|
read_multi_mget(*names).tap do |results|
payload[:hits] = results.keys
end
end
else
super
end
end
# Cache Store API implementation.
#
# Supports Redis KEYS glob patterns:
#
# h?llo matches hello, hallo and hxllo
# h*llo matches hllo and heeeello
# h[ae]llo matches hello and hallo, but not hillo
# h[^e]llo matches hallo, hbllo, ... but not hello
# h[a-b]llo matches hallo and hbllo
#
# Use \ to escape special characters if you want to match them verbatim.
#
# See https://redis.io/commands/KEYS for more.
#
# Failsafe: Raises errors.
- 3
def delete_matched(matcher, options = nil)
instrument :delete_matched, matcher do
unless String === matcher
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end
redis.with do |c|
pattern = namespace_key(matcher, options)
cursor = "0"
# Fetch keys in batches using SCAN to avoid blocking the Redis server.
nodes = c.respond_to?(:nodes) ? c.nodes : [c]
nodes.each do |node|
begin
cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
node.del(*keys) unless keys.empty?
end until cursor == "0"
end
end
end
end
# Cache Store API implementation.
#
# Increment a cached value. This method uses the Redis incr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
#
# Failsafe: Raises errors.
- 3
def increment(name, amount = 1, options = nil)
instrument :increment, name, amount: amount do
failsafe :increment do
options = merged_options(options)
key = normalize_key(name, options)
redis.with do |c|
c.incrby(key, amount).tap do
write_key_expiry(c, key, options)
end
end
end
end
end
# Cache Store API implementation.
#
# Decrement a cached value. This method uses the Redis decr atomic
# operator and can only be used on values written with the :raw option.
# Calling it on a value not stored with :raw will initialize that value
# to zero.
#
# Failsafe: Raises errors.
- 3
def decrement(name, amount = 1, options = nil)
instrument :decrement, name, amount: amount do
failsafe :decrement do
options = merged_options(options)
key = normalize_key(name, options)
redis.with do |c|
c.decrby(key, amount).tap do
write_key_expiry(c, key, options)
end
end
end
end
end
# Cache Store API implementation.
#
# Removes expired entries. Handled natively by Redis least-recently-/
# least-frequently-used expiry, so manual cleanup is not supported.
- 3
def cleanup(options = nil)
super
end
# Clear the entire cache on all Redis servers. Safe to use on
# shared servers if the cache is namespaced.
#
# Failsafe: Raises errors.
- 3
def clear(options = nil)
failsafe :clear do
if namespace = merged_options(options)[:namespace]
delete_matched "*", namespace: namespace
else
redis.with { |c| c.flushdb }
end
end
end
- 3
def mget_capable? #:nodoc:
set_redis_capabilities unless defined? @mget_capable
@mget_capable
end
- 3
def mset_capable? #:nodoc:
set_redis_capabilities unless defined? @mset_capable
@mset_capable
end
- 3
private
- 3
def set_redis_capabilities
case redis
when Redis::Distributed
@mget_capable = true
@mset_capable = false
else
@mget_capable = true
@mset_capable = true
end
end
# Store provider interface:
# Read an entry from the cache.
- 3
def read_entry(key, **options)
failsafe :read_entry do
raw = options&.fetch(:raw, false)
deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
end
end
- 3
def read_multi_entries(names, **options)
if mget_capable?
read_multi_mget(*names, **options)
else
super
end
end
- 3
def read_multi_mget(*names)
options = names.extract_options!
options = merged_options(options)
return {} if names == []
raw = options&.fetch(:raw, false)
keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:read_multi_mget, returning: {}) do
redis.with { |c| c.mget(*keys) }
end
names.zip(values).each_with_object({}) do |(name, value), results|
if value
entry = deserialize_entry(value, raw: raw)
unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
results[name] = entry.value
end
end
end
end
# Write an entry to the cache.
#
# Requires Redis 2.6.12+ for extended SET options.
- 3
def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
serialized_entry = serialize_entry(entry, raw: raw)
# If race condition TTL is in use, ensure that cache entries
# stick around a bit longer after they would have expired
# so we can purposefully serve stale entries.
if race_condition_ttl && expires_in && expires_in > 0 && !raw
expires_in += 5.minutes
end
failsafe :write_entry, returning: false do
if unless_exist || expires_in
modifiers = {}
modifiers[:nx] = unless_exist
modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
redis.with { |c| c.set key, serialized_entry, **modifiers }
else
redis.with { |c| c.set key, serialized_entry }
end
end
end
- 3
def write_key_expiry(client, key, options)
if options[:expires_in] && client.ttl(key).negative?
client.expire key, options[:expires_in].to_i
end
end
# Delete an entry from the cache.
- 3
def delete_entry(key, options)
failsafe :delete_entry, returning: false do
redis.with { |c| c.del key }
end
end
# Deletes multiple entries in the cache. Returns the number of entries deleted.
- 3
def delete_multi_entries(entries, **_options)
redis.with { |c| c.del(entries) }
end
# Nonstandard store provider API to write multiple values at once.
- 3
def write_multi_entries(entries, expires_in: nil, **options)
if entries.any?
if mset_capable? && expires_in.nil?
failsafe :write_multi_entries do
redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
end
else
super
end
end
end
# Truncate keys that exceed 1kB.
- 3
def normalize_key(key, options)
truncate_key super&.b
end
- 3
def truncate_key(key)
if key && key.bytesize > max_key_bytesize
suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
truncate_at = max_key_bytesize - suffix.bytesize
"#{key.byteslice(0, truncate_at)}#{suffix}"
else
key
end
end
- 3
def deserialize_entry(serialized_entry, raw:)
if serialized_entry
if raw
Entry.new(serialized_entry, compress: false)
else
Marshal.load(serialized_entry)
end
end
end
- 3
def serialize_entry(entry, raw: false)
if raw
entry.value.to_s
else
Marshal.dump(entry)
end
end
- 3
def serialize_entries(entries, raw: false)
entries.transform_values do |entry|
serialize_entry entry, raw: raw
end
end
- 3
def failsafe(method, returning: nil)
yield
rescue ::Redis::BaseError => e
handle_exception exception: e, method: method, returning: returning
returning
end
- 3
def handle_exception(exception:, method:, returning:)
if @error_handler
@error_handler.(method: method, exception: exception, returning: returning)
end
rescue => failsafe
warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
end
end
end
end
# frozen_string_literal: true
- 4
require "active_support/core_ext/string/inflections"
- 4
require "active_support/per_thread_registry"
- 4
module ActiveSupport
- 4
module Cache
- 4
module Strategy
# Caches that implement LocalCache will be backed by an in-memory cache for the
# duration of a block. Repeated calls to the cache for the same key will hit the
# in-memory cache for faster access.
- 4
module LocalCache
- 4
autoload :Middleware, "active_support/cache/strategy/local_cache_middleware"
# Class for storing and registering the local caches.
- 4
class LocalCacheRegistry # :nodoc:
- 4
extend ActiveSupport::PerThreadRegistry
- 4
def initialize
@registry = {}
end
- 4
def cache_for(local_cache_key)
@registry[local_cache_key]
end
- 4
def set_cache_for(local_cache_key, value)
@registry[local_cache_key] = value
end
- 4
def self.set_cache_for(l, v); instance.set_cache_for l, v; end
- 4
def self.cache_for(l); instance.cache_for l; end
end
# Simple memory backed cache. This cache is not thread safe and is intended only
# for serving as a temporary memory cache for a single thread.
- 4
class LocalStore < Store
- 4
def initialize
super
@data = {}
end
# Don't allow synchronizing since it isn't thread safe.
- 4
def synchronize # :nodoc:
yield
end
- 4
def clear(options = nil)
@data.clear
end
- 4
def read_entry(key, **options)
@data[key]
end
- 4
def read_multi_entries(keys, **options)
values = {}
keys.each do |name|
entry = read_entry(name, **options)
values[name] = entry.value if entry
end
values
end
- 4
def write_entry(key, entry, **options)
entry.dup_value!
@data[key] = entry
true
end
- 4
def delete_entry(key, **options)
!!@data.delete(key)
end
- 4
def fetch_entry(key, options = nil) # :nodoc:
entry = @data.fetch(key) { @data[key] = yield }
dup_entry = entry.dup
dup_entry&.dup_value!
dup_entry
end
end
# Use a local cache for the duration of block.
- 4
def with_local_cache
use_temporary_local_cache(LocalStore.new) { yield }
end
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
- 4
def middleware
@middleware ||= Middleware.new(
"ActiveSupport::Cache::Strategy::LocalCache",
local_cache_key)
end
- 4
def clear(**options) # :nodoc:
return super unless cache = local_cache
cache.clear(options)
super
end
- 4
def cleanup(**options) # :nodoc:
return super unless cache = local_cache
cache.clear
super
end
- 4
def increment(name, amount = 1, **options) # :nodoc:
return super unless local_cache
value = bypass_local_cache { super }
write_cache_value(name, value, **options)
value
end
- 4
def decrement(name, amount = 1, **options) # :nodoc:
return super unless local_cache
value = bypass_local_cache { super }
write_cache_value(name, value, **options)
value
end
- 4
private
- 4
def read_entry(key, **options)
if cache = local_cache
cache.fetch_entry(key) { super }
else
super
end
end
- 4
def read_multi_entries(keys, **options)
return super unless local_cache
local_entries = local_cache.read_multi_entries(keys, **options)
missed_keys = keys - local_entries.keys
if missed_keys.any?
local_entries.merge!(super(missed_keys, **options))
else
local_entries
end
end
- 4
def write_entry(key, entry, **options)
if options[:unless_exist]
local_cache.delete_entry(key, **options) if local_cache
else
local_cache.write_entry(key, entry, **options) if local_cache
end
super
end
- 4
def delete_entry(key, **options)
local_cache.delete_entry(key, **options) if local_cache
super
end
- 4
def write_cache_value(name, value, **options)
name = normalize_key(name, options)
cache = local_cache
cache.mute do
if value
cache.write(name, value, options)
else
cache.delete(name, **options)
end
end
end
- 4
def local_cache_key
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
end
- 4
def local_cache
LocalCacheRegistry.cache_for(local_cache_key)
end
- 4
def bypass_local_cache
use_temporary_local_cache(nil) { yield }
end
- 4
def use_temporary_local_cache(temporary_cache)
save_cache = LocalCacheRegistry.cache_for(local_cache_key)
begin
LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache)
yield
ensure
LocalCacheRegistry.set_cache_for(local_cache_key, save_cache)
end
end
end
end
end
end
# frozen_string_literal: true
require "rack/body_proxy"
require "rack/utils"
module ActiveSupport
module Cache
module Strategy
module LocalCache
#--
# This class wraps up local storage for middlewares. Only the middleware method should
# construct them.
class Middleware # :nodoc:
attr_reader :name, :local_cache_key
def initialize(name, local_cache_key)
@name = name
@local_cache_key = local_cache_key
@app = nil
end
def new(app)
@app = app
self
end
def call(env)
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
cleanup_on_body_close = true
response
rescue Rack::Utils::InvalidParameterError
[400, {}, []]
ensure
LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
cleanup_on_body_close
end
end
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/concern"
- 23
require "active_support/descendants_tracker"
- 23
require "active_support/core_ext/array/extract_options"
- 23
require "active_support/core_ext/class/attribute"
- 23
require "active_support/core_ext/string/filters"
- 23
require "thread"
- 23
module ActiveSupport
# Callbacks are code hooks that are run at key points in an object's life cycle.
# The typical use case is to have a base class define a set of callbacks
# relevant to the other functionality it supplies, so that subclasses can
# install callbacks that enhance or modify the base functionality without
# needing to override or redefine methods of the base class.
#
# Mixing in this module allows you to define the events in the object's
# life cycle that will support callbacks (via +ClassMethods.define_callbacks+),
# set the instance methods, procs, or callback objects to be called (via
# +ClassMethods.set_callback+), and run the installed callbacks at the
# appropriate times (via +run_callbacks+).
#
# By default callbacks are halted by throwing +:abort+.
# See +ClassMethods.define_callbacks+ for details.
#
# Three kinds of callbacks are supported: before callbacks, run before a
# certain event; after callbacks, run after the event; and around callbacks,
# blocks that surround the event, triggering it when they yield. Callback code
# can be contained in instance methods, procs or lambdas, or callback objects
# that respond to certain predetermined methods. See +ClassMethods.set_callback+
# for details.
#
# class Record
# include ActiveSupport::Callbacks
# define_callbacks :save
#
# def save
# run_callbacks :save do
# puts "- save"
# end
# end
# end
#
# class PersonRecord < Record
# set_callback :save, :before, :saving_message
# def saving_message
# puts "saving..."
# end
#
# set_callback :save, :after do |object|
# puts "saved"
# end
# end
#
# person = PersonRecord.new
# person.save
#
# Output:
# saving...
# - save
# saved
- 23
module Callbacks
- 23
extend Concern
- 23
included do
- 40
extend ActiveSupport::DescendantsTracker
- 40
class_attribute :__callbacks, instance_writer: false, default: {}
end
- 23
CALLBACK_FILTER_TYPES = [:before, :after, :around]
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
# the block (if given one), and then runs the after callbacks in reverse
# order.
#
# If the callback chain was halted, returns +false+. Otherwise returns the
# result of the block, +nil+ if no callbacks have been set, or +true+
# if callbacks have been set but no block is given.
#
# run_callbacks :save do
# save
# end
#
#--
#
# As this method is used in many places, and often wraps large portions of
# user code, it has an additional design goal of minimizing its impact on
# the visible call stack. An exception from inside a :before or :after
# callback can be as noisy as it likes -- but when control has passed
# smoothly through and into the supplied block, we want as little evidence
# as possible that we were here.
- 23
def run_callbacks(kind)
callbacks = __callbacks[kind.to_sym]
if callbacks.empty?
yield if block_given?
else
env = Filters::Environment.new(self, false, nil)
next_sequence = callbacks.compile
# Common case: no 'around' callbacks defined
if next_sequence.final?
next_sequence.invoke_before(env)
env.value = !env.halted && (!block_given? || yield)
next_sequence.invoke_after(env)
env.value
else
invoke_sequence = Proc.new do
skipped = nil
while true
current = next_sequence
current.invoke_before(env)
if current.final?
env.value = !env.halted && (!block_given? || yield)
elsif current.skip?(env)
(skipped ||= []) << current
next_sequence = next_sequence.nested
next
else
next_sequence = next_sequence.nested
begin
target, block, method, *arguments = current.expand_call_template(env, invoke_sequence)
target.send(method, *arguments, &block)
ensure
next_sequence = current
end
end
current.invoke_after(env)
skipped.pop.invoke_after(env) while skipped&.first
break env.value
end
end
invoke_sequence.call
end
end
end
- 23
private
# A hook invoked every time a before callback is halted.
# This can be overridden in ActiveSupport::Callbacks implementors in order
# to provide better debugging/logging.
- 23
def halted_callback_hook(filter, name)
end
- 23
module Conditionals # :nodoc:
- 23
class Value
- 23
def initialize(&block)
@block = block
end
- 23
def call(target, value); @block.call(value); end
end
end
- 23
module Filters
- 23
Environment = Struct.new(:target, :halted, :value)
- 23
class Before
- 23
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter, name)
halted_lambda = chain_config[:terminator]
if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
else
halting(callback_sequence, user_callback, halted_lambda, filter, name)
end
end
- 23
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter, name
end
end
env
end
end
- 23
private_class_method :halting_and_conditional
- 23
def self.halting(callback_sequence, user_callback, halted_lambda, filter, name)
callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
unless halted
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter, name
end
end
env
end
end
- 23
private_class_method :halting
end
- 23
class After
- 23
def self.build(callback_sequence, user_callback, user_conditions, chain_config)
if chain_config[:skip_after_callbacks_if_terminated]
if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions)
else
halting(callback_sequence, user_callback)
end
else
if user_conditions.any?
conditional callback_sequence, user_callback, user_conditions
else
simple callback_sequence, user_callback
end
end
end
- 23
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
callback_sequence.after do |env|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
env
end
end
- 23
private_class_method :halting_and_conditional
- 23
def self.halting(callback_sequence, user_callback)
callback_sequence.after do |env|
unless env.halted
user_callback.call env.target, env.value
end
env
end
end
- 23
private_class_method :halting
- 23
def self.conditional(callback_sequence, user_callback, user_conditions)
callback_sequence.after do |env|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
env
end
end
- 23
private_class_method :conditional
- 23
def self.simple(callback_sequence, user_callback)
callback_sequence.after do |env|
user_callback.call env.target, env.value
env
end
end
- 23
private_class_method :simple
end
end
- 23
class Callback #:nodoc:#
- 23
def self.build(chain, filter, kind, options)
- 154
if filter.is_a?(String)
raise ArgumentError, <<-MSG.squish
Passing string to define a callback is not supported. See the `.set_callback`
documentation to see supported values.
MSG
end
- 154
new chain.name, filter, kind, options, chain.config
end
- 23
attr_accessor :kind, :name
- 23
attr_reader :chain_config
- 23
def initialize(name, filter, kind, options, chain_config)
- 154
@chain_config = chain_config
- 154
@name = name
- 154
@kind = kind
- 154
@filter = filter
- 154
@key = compute_identifier filter
- 154
@if = check_conditionals(options[:if])
- 154
@unless = check_conditionals(options[:unless])
end
- 265
def filter; @key; end
- 23
def raw_filter; @filter; end
- 23
def merge_conditional_options(chain, if_option:, unless_option:)
- 13
options = {
if: @if.dup,
unless: @unless.dup
}
- 13
options[:if].concat Array(unless_option)
- 13
options[:unless].concat Array(if_option)
- 13
self.class.build chain, @filter, @kind, options
end
- 23
def matches?(_kind, _filter)
- 196
@kind == _kind && filter == _filter
end
- 23
def duplicates?(other)
- 253
case @filter
when Symbol
- 158
matches?(other.kind, other.filter)
else
- 95
false
end
end
# Wraps code with filter
- 23
def apply(callback_sequence)
user_conditions = conditions_lambdas
user_callback = CallTemplate.build(@filter, self)
case kind
when :before
Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter, name)
when :after
Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
when :around
callback_sequence.around(user_callback, user_conditions)
end
end
- 23
def current_scopes
Array(chain_config[:scope]).map { |s| public_send(s) }
end
- 23
private
- 23
EMPTY_ARRAY = [].freeze
- 23
private_constant :EMPTY_ARRAY
- 23
def check_conditionals(conditionals)
- 308
return EMPTY_ARRAY if conditionals.blank?
- 51
conditionals = Array(conditionals)
- 109
if conditionals.any? { |c| c.is_a?(String) }
raise ArgumentError, <<-MSG.squish
Passing string to be evaluated in :if and :unless conditional
options is not supported. Pass a symbol for an instance method,
or a lambda, proc or block, instead.
MSG
end
- 51
conditionals.freeze
end
- 23
def compute_identifier(filter)
- 154
case filter
when ::Proc
- 58
filter.object_id
else
- 96
filter
end
end
- 23
def conditions_lambdas
@if.map { |c| CallTemplate.build(c, self).make_lambda } +
@unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
end
end
# A future invocation of user-supplied code (either as a callback,
# or a condition filter).
- 23
class CallTemplate # :nodoc:
- 23
def initialize(target, method, arguments, block)
@override_target = target
@method_name = method
@arguments = arguments
@override_block = block
end
# Return the parts needed to make this call, with the given
# input values.
#
# Returns an array of the form:
#
# [target, block, method, *arguments]
#
# This array can be used as such:
#
# target.send(method, *arguments, &block)
#
# The actual invocation is left up to the caller to minimize
# call stack pollution.
- 23
def expand(target, value, block)
expanded = [@override_target || target, @override_block || block, @method_name]
@arguments.each do |arg|
case arg
when :value then expanded << value
when :target then expanded << target
when :block then expanded << (block || raise(ArgumentError))
end
end
expanded
end
# Return a lambda that will make this call when given the input
# values.
- 23
def make_lambda
lambda do |target, value, &block|
target, block, method, *arguments = expand(target, value, block)
target.send(method, *arguments, &block)
end
end
# Return a lambda that will make this call when given the input
# values, but then return the boolean inverse of that result.
- 23
def inverted_lambda
lambda do |target, value, &block|
target, block, method, *arguments = expand(target, value, block)
! target.send(method, *arguments, &block)
end
end
# Filters support:
#
# Symbols:: A method to call.
# Procs:: A proc to call with the object.
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
#
# All of these objects are converted into a CallTemplate and handled
# the same after this point.
- 23
def self.build(filter, callback)
case filter
when Symbol
new(nil, filter, [], nil)
when Conditionals::Value
new(filter, :call, [:target, :value], nil)
when ::Proc
if filter.arity > 1
new(nil, :instance_exec, [:target, :block], filter)
elsif filter.arity > 0
new(nil, :instance_exec, [:target], filter)
else
new(nil, :instance_exec, [], filter)
end
else
method_to_call = callback.current_scopes.join("_")
new(filter, method_to_call, [:target], nil)
end
end
end
# Execute before and after filters in a sequence instead of
# chaining them with nested lambda calls, see:
# https://github.com/rails/rails/issues/18011
- 23
class CallbackSequence # :nodoc:
- 23
def initialize(nested = nil, call_template = nil, user_conditions = nil)
@nested = nested
@call_template = call_template
@user_conditions = user_conditions
@before = []
@after = []
end
- 23
def before(&before)
@before.unshift(before)
self
end
- 23
def after(&after)
@after.push(after)
self
end
- 23
def around(call_template, user_conditions)
CallbackSequence.new(self, call_template, user_conditions)
end
- 23
def skip?(arg)
arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
end
- 23
attr_reader :nested
- 23
def final?
!@call_template
end
- 23
def expand_call_template(arg, block)
@call_template.expand(arg.target, arg.value, block)
end
- 23
def invoke_before(arg)
@before.each { |b| b.call(arg) }
end
- 23
def invoke_after(arg)
@after.each { |a| a.call(arg) }
end
end
- 23
class CallbackChain #:nodoc:#
- 23
include Enumerable
- 23
attr_reader :name, :config
- 23
def initialize(name, config)
- 66
@name = name
- 66
@config = {
scope: [:kind],
terminator: default_terminator
}.merge!(config)
- 66
@chain = []
- 66
@callbacks = nil
- 66
@mutex = Mutex.new
end
- 37
def each(&block); @chain.each(&block); end
- 36
def index(o); @chain.index(o); end
- 23
def empty?; @chain.empty?; end
- 23
def insert(index, o)
- 13
@callbacks = nil
- 13
@chain.insert(index, o)
end
- 23
def delete(o)
- 14
@callbacks = nil
- 14
@chain.delete(o)
end
- 23
def clear
- 1
@callbacks = nil
- 1
@chain.clear
- 1
self
end
- 23
def initialize_copy(other)
- 145
@callbacks = nil
- 145
@chain = other.chain.dup
- 145
@mutex = Mutex.new
end
- 23
def compile
@callbacks || @mutex.synchronize do
final_sequence = CallbackSequence.new
@callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
callback.apply callback_sequence
end
end
end
- 23
def append(*callbacks)
- 271
callbacks.each { |c| append_one(c) }
end
- 23
def prepend(*callbacks)
callbacks.each { |c| prepend_one(c) }
end
- 23
protected
- 23
attr_reader :chain
- 23
private
- 23
def append_one(callback)
- 141
@callbacks = nil
- 141
remove_duplicates(callback)
- 141
@chain.push(callback)
end
- 23
def prepend_one(callback)
@callbacks = nil
remove_duplicates(callback)
@chain.unshift(callback)
end
- 23
def remove_duplicates(callback)
- 141
@callbacks = nil
- 394
@chain.delete_if { |c| callback.duplicates?(c) }
end
- 23
def default_terminator
- 66
Proc.new do |target, result_lambda|
terminate = true
catch(:abort) do
result_lambda.call
terminate = false
end
terminate
end
end
end
- 23
module ClassMethods
- 23
def normalize_callback_params(filters, block) # :nodoc:
- 144
type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
- 144
options = filters.extract_options!
- 144
filters.unshift(block) if block
- 144
[type, filters, options.dup]
end
# This is used internally to append, prepend and skip callbacks to the
# CallbackChain.
- 23
def __update_callbacks(name) #:nodoc:
- 144
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
- 144
chain = target.get_callbacks name
- 144
yield target, chain.dup
end
end
# Install a callback for the given event.
#
# set_callback :save, :before, :before_method
# set_callback :save, :after, :after_method, if: :condition
# set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
# The second argument indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
# means the first example above can also be written as:
#
# set_callback :save, :before_method
#
# The callback can be specified as a symbol naming an instance method; as a
# proc, lambda, or block; or as an object that responds to a certain method
# determined by the <tt>:scope</tt> argument to +define_callbacks+.
#
# If a proc, lambda, or block is given, its body is evaluated in the context
# of the current object. It can also optionally accept the current object as
# an argument.
#
# Before and around callbacks are called in the order that they are set;
# after callbacks are called in the reverse order.
#
# Around callbacks can access the return value from the event, if it
# wasn't halted, from the +yield+ call.
#
# ===== Options
#
# * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance
# method or a proc; the callback will be called only when they all return
# a true value.
#
# If a proc is given, its body is evaluated in the context of the
# current object. It can also optionally accept the current object as
# an argument.
# * <tt>:unless</tt> - A symbol or an array of symbols, each naming an
# instance method or a proc; the callback will be called only when they
# all return a false value.
#
# If a proc is given, its body is evaluated in the context of the
# current object. It can also optionally accept the current object as
# an argument.
# * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
# existing chain rather than appended.
- 23
def set_callback(name, *filter_list, &block)
- 130
type, filters, options = normalize_callback_params(filter_list, block)
- 130
self_chain = get_callbacks name
- 130
mapped = filters.map do |filter|
- 141
Callback.build(self_chain, filter, type, options)
end
- 130
__update_callbacks(name) do |target, chain|
- 130
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
- 130
target.set_callbacks name, chain
end
end
# Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
# <tt>:unless</tt> options may be passed in order to control when the
# callback is skipped.
#
# class Writer < Person
# skip_callback :validate, :before, :check_membership, if: -> { age > 18 }
# end
#
# An <tt>ArgumentError</tt> will be raised if the callback has not
# already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
- 23
def skip_callback(name, *filter_list, &block)
- 14
type, filters, options = normalize_callback_params(filter_list, block)
- 14
options[:raise] = true unless options.key?(:raise)
- 14
__update_callbacks(name) do |target, chain|
- 14
filters.each do |filter|
- 52
callback = chain.find { |c| c.matches?(type, filter) }
- 14
if !callback && options[:raise]
raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
end
- 14
if callback && (options.key?(:if) || options.key?(:unless))
- 13
new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
- 13
chain.insert(chain.index(callback), new_callback)
end
- 14
chain.delete(callback)
end
- 14
target.set_callbacks name, chain
end
end
# Remove all set callbacks for the given event.
- 23
def reset_callbacks(name)
- 1
callbacks = get_callbacks name
- 1
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
chain = target.get_callbacks(name).dup
callbacks.each { |c| chain.delete(c) }
target.set_callbacks name, chain
end
- 1
set_callbacks(name, callbacks.dup.clear)
end
# Define sets of events in the object life cycle that support callbacks.
#
# define_callbacks :validate
# define_callbacks :initialize, :save, :destroy
#
# ===== Options
#
# * <tt>:terminator</tt> - Determines when a before filter will halt the
# callback chain, preventing following before and around callbacks from
# being called and the event from being triggered.
# This should be a lambda to be executed.
# The current object and the result lambda of the callback will be provided
# to the terminator lambda.
#
# define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false }
#
# In this example, if any before validate callbacks returns +false+,
# any successive before and around callback is not executed.
#
# 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
# default after callbacks are executed no matter if callback chain was
# terminated or not. This option has no effect if <tt>:terminator</tt>
# option is set to +nil+.
#
# * <tt>:scope</tt> - Indicates which methods should be executed when an
# object is used as a callback.
#
# class Audit
# def before(caller)
# puts 'Audit: before is called'
# end
#
# def before_save(caller)
# puts 'Audit: before_save is called'
# end
# end
#
# class Account
# include ActiveSupport::Callbacks
#
# define_callbacks :save
# set_callback :save, :before, Audit.new
#
# def save
# run_callbacks :save do
# puts 'save in main'
# end
# end
# end
#
# In the above case whenever you save an account the method
# <tt>Audit#before</tt> will be called. On the other hand
#
# define_callbacks :save, scope: [:kind, :name]
#
# would trigger <tt>Audit#before_save</tt> instead. That's constructed
# by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
# case "kind" is "before" and "name" is "save". In this context +:kind+
# and +:name+ have special meanings: +:kind+ refers to the kind of
# callback (before/after/around) and +:name+ refers to the method on
# which callbacks are being defined.
#
# A declaration like
#
# define_callbacks :save, scope: [:name]
#
# would call <tt>Audit#save</tt>.
#
# ===== Notes
#
# +names+ passed to +define_callbacks+ must not end with
# <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
#
# Calling +define_callbacks+ multiple times with the same +names+ will
# overwrite previous callbacks registered with +set_callback+.
- 23
def define_callbacks(*names)
- 43
options = names.extract_options!
- 43
names.each do |name|
- 66
name = name.to_sym
- 66
([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
- 66
target.set_callbacks name, CallbackChain.new(name, options)
end
- 66
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
run_callbacks #{name.inspect}, &block
end
def self._#{name}_callbacks
get_callbacks(#{name.inspect})
end
def self._#{name}_callbacks=(value)
set_callbacks(#{name.inspect}, value)
end
def _#{name}_callbacks
__callbacks[#{name.inspect}]
end
RUBY
end
end
- 23
protected
- 23
def get_callbacks(name) # :nodoc:
- 275
__callbacks[name.to_sym]
end
- 23
if Module.instance_method(:method_defined?).arity == 1 # Ruby 2.5 and older
- 23
def set_callbacks(name, callbacks) # :nodoc:
- 211
self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
end
else # Ruby 2.6 and newer
def set_callbacks(name, callbacks) # :nodoc:
unless singleton_class.method_defined?(:__callbacks, false)
self.__callbacks = __callbacks.dup
end
self.__callbacks[name.to_sym] = callbacks
self.__callbacks
end
end
end
end
end
# frozen_string_literal: true
- 24
module ActiveSupport
# A typical module looks like this:
#
# module M
# def self.included(base)
# base.extend ClassMethods
# base.class_eval do
# scope :disabled, -> { where(disabled: true) }
# end
# end
#
# module ClassMethods
# ...
# end
# end
#
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
# written as:
#
# require "active_support/concern"
#
# module M
# extend ActiveSupport::Concern
#
# included do
# scope :disabled, -> { where(disabled: true) }
# end
#
# class_methods do
# ...
# end
# end
#
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
# and a +Bar+ module which depends on the former, we would typically write the
# following:
#
# module Foo
# def self.included(base)
# base.class_eval do
# def self.method_injected_by_foo
# ...
# end
# end
# end
# end
#
# module Bar
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# include Foo # We need to include this dependency for Bar
# include Bar # Bar is the module that Host really needs
# end
#
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
#
# module Bar
# include Foo
# def self.included(base)
# base.method_injected_by_foo
# end
# end
#
# class Host
# include Bar
# end
#
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
# module dependencies are properly resolved:
#
# require "active_support/concern"
#
# module Foo
# extend ActiveSupport::Concern
# included do
# def self.method_injected_by_foo
# ...
# end
# end
# end
#
# module Bar
# extend ActiveSupport::Concern
# include Foo
#
# included do
# self.method_injected_by_foo
# end
# end
#
# class Host
# include Bar # It works, now Bar takes care of its dependencies
# end
#
# === Prepending concerns
#
# Just like `include`, concerns also support `prepend` with a corresponding
# `prepended do` callback. `module ClassMethods` or `class_methods do` are
# prepended as well.
#
# `prepend` is also used for any dependencies.
- 24
module Concern
- 24
class MultipleIncludedBlocks < StandardError #:nodoc:
- 24
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
- 24
class MultiplePrependBlocks < StandardError #:nodoc:
- 24
def initialize
super "Cannot define multiple 'prepended' blocks for a Concern"
end
end
- 24
def self.extended(base) #:nodoc:
- 149
base.instance_variable_set(:@_dependencies, [])
end
- 24
def append_features(base) #:nodoc:
- 124
if base.instance_variable_defined?(:@_dependencies)
- 3
base.instance_variable_get(:@_dependencies) << self
- 3
false
else
- 121
return false if base < self
- 120
@_dependencies.each { |dep| base.include(dep) }
- 120
super
- 120
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
- 120
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
- 24
def prepend_features(base) #:nodoc:
- 1
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies).unshift self
false
else
- 1
return false if base < self
- 1
@_dependencies.each { |dep| base.prepend(dep) }
- 1
super
- 1
base.singleton_class.prepend const_get(:ClassMethods) if const_defined?(:ClassMethods)
- 1
base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
end
end
# Evaluate given block in context of base class,
# so that you can write class macros here.
# When you define more than one +included+ block, it raises an exception.
- 24
def included(base = nil, &block)
- 245
if base.nil?
- 121
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
- 121
@_included_block = block
end
else
- 124
super
end
end
# Evaluate given block in context of base class,
# so that you can write class macros here.
# When you define more than one +prepended+ block, it raises an exception.
- 24
def prepended(base = nil, &block)
- 2
if base.nil?
- 1
if instance_variable_defined?(:@_prepended_block)
if @_prepended_block.source_location != block.source_location
raise MultiplePrependBlocks
end
else
- 1
@_prepended_block = block
end
else
- 1
super
end
end
# Define class methods from given block.
# You can define private class methods as well.
#
# module Example
# extend ActiveSupport::Concern
#
# class_methods do
# def foo; puts 'foo'; end
#
# private
# def bar; puts 'bar'; end
# end
# end
#
# class Buzz
# include Example
# end
#
# Buzz.foo # => "foo"
# Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)
- 24
def class_methods(&class_methods_module_definition)
- 5
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
- 5
mod.module_eval(&class_methods_module_definition)
end
end
end
# frozen_string_literal: true
- 1
require "monitor"
- 1
module ActiveSupport
- 1
module Concurrency
# A monitor that will permit dependency loading while blocked waiting for
# the lock.
- 1
class LoadInterlockAwareMonitor < Monitor
- 1
EXCEPTION_NEVER = { Exception => :never }.freeze
- 1
EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
- 1
private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
# Enters an exclusive section, but allows dependency loading while blocked
- 1
def mon_enter
mon_try_enter ||
ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
end
- 1
def synchronize
Thread.handle_interrupt(EXCEPTION_NEVER) do
mon_enter
begin
Thread.handle_interrupt(EXCEPTION_IMMEDIATE) do
yield
end
ensure
mon_exit
end
end
end
end
end
end
# frozen_string_literal: true
- 3
require "thread"
- 3
require "monitor"
- 3
module ActiveSupport
- 3
module Concurrency
# A share/exclusive lock, otherwise known as a read/write lock.
#
# https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
- 3
class ShareLock
- 3
include MonitorMixin
# We track Thread objects, instead of just using counters, because
# we need exclusive locks to be reentrant, and we need to be able
# to upgrade share locks to exclusive.
- 3
def raw_state # :nodoc:
synchronize do
threads = @sleeping.keys | @sharing.keys | @waiting.keys
threads |= [@exclusive_thread] if @exclusive_thread
data = {}
threads.each do |thread|
purpose, compatible = @waiting[thread]
data[thread] = {
thread: thread,
sharing: @sharing[thread],
exclusive: @exclusive_thread == thread,
purpose: purpose,
compatible: compatible,
waiting: !!@waiting[thread],
sleeper: @sleeping[thread],
}
end
# NB: Yields while holding our *internal* synchronize lock,
# which is supposed to be used only for a few instructions at
# a time. This allows the caller to inspect additional state
# without things changing out from underneath, but would have
# disastrous effects upon normal operation. Fortunately, this
# method is only intended to be called when things have
# already gone wrong.
yield data
end
end
- 3
def initialize
- 3
super()
- 3
@cv = new_cond
- 3
@sharing = Hash.new(0)
- 3
@waiting = {}
- 3
@sleeping = {}
- 3
@exclusive_thread = nil
- 3
@exclusive_depth = 0
end
# Returns false if +no_wait+ is set and the lock is not
# immediately available. Otherwise, returns true after the lock
# has been acquired.
#
# +purpose+ and +compatible+ work together; while this thread is
# waiting for the exclusive lock, it will yield its share (if any)
# to any other attempt whose +purpose+ appears in this attempt's
# +compatible+ list. This allows a "loose" upgrade, which, being
# less strict, prevents some classes of deadlocks.
#
# For many resources, loose upgrades are sufficient: if a thread
# is awaiting a lock, it is not running any other code. With
# +purpose+ matching, it is possible to yield only to other
# threads whose activity will not interfere.
- 3
def start_exclusive(purpose: nil, compatible: [], no_wait: false)
synchronize do
unless @exclusive_thread == Thread.current
if busy_for_exclusive?(purpose)
return false if no_wait
yield_shares(purpose: purpose, compatible: compatible, block_share: true) do
wait_for(:start_exclusive) { busy_for_exclusive?(purpose) }
end
end
@exclusive_thread = Thread.current
end
@exclusive_depth += 1
true
end
end
# Relinquish the exclusive lock. Must only be called by the thread
# that called start_exclusive (and currently holds the lock).
- 3
def stop_exclusive(compatible: [])
synchronize do
raise "invalid unlock" if @exclusive_thread != Thread.current
@exclusive_depth -= 1
if @exclusive_depth == 0
@exclusive_thread = nil
if eligible_waiters?(compatible)
yield_shares(compatible: compatible, block_share: true) do
wait_for(:stop_exclusive) { @exclusive_thread || eligible_waiters?(compatible) }
end
end
@cv.broadcast
end
end
end
- 3
def start_sharing
synchronize do
if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current
# We already hold a lock; nothing to wait for
elsif @waiting[Thread.current]
# We're nested inside a +yield_shares+ call: we'll resume as
# soon as there isn't an exclusive lock in our way
wait_for(:start_sharing) { @exclusive_thread }
else
# This is an initial / outermost share call: any outstanding
# requests for an exclusive lock get to go first
wait_for(:start_sharing) { busy_for_sharing?(false) }
end
@sharing[Thread.current] += 1
end
end
- 3
def stop_sharing
synchronize do
if @sharing[Thread.current] > 1
@sharing[Thread.current] -= 1
else
@sharing.delete Thread.current
@cv.broadcast
end
end
end
# Execute the supplied block while holding the Exclusive lock. If
# +no_wait+ is set and the lock is not immediately available,
# returns +nil+ without yielding. Otherwise, returns the result of
# the block.
#
# See +start_exclusive+ for other options.
- 3
def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false)
if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait)
begin
yield
ensure
stop_exclusive(compatible: after_compatible)
end
end
end
# Execute the supplied block while holding the Share lock.
- 3
def sharing
start_sharing
begin
yield
ensure
stop_sharing
end
end
# Temporarily give up all held Share locks while executing the
# supplied block, allowing any +compatible+ exclusive lock request
# to proceed.
- 3
def yield_shares(purpose: nil, compatible: [], block_share: false)
loose_shares = previous_wait = nil
synchronize do
if loose_shares = @sharing.delete(Thread.current)
if previous_wait = @waiting[Thread.current]
purpose = nil unless purpose == previous_wait[0]
compatible &= previous_wait[1]
end
compatible |= [false] unless block_share
@waiting[Thread.current] = [purpose, compatible]
end
@cv.broadcast
end
begin
yield
ensure
synchronize do
wait_for(:yield_shares) { @exclusive_thread && @exclusive_thread != Thread.current }
if previous_wait
@waiting[Thread.current] = previous_wait
else
@waiting.delete Thread.current
end
@sharing[Thread.current] = loose_shares if loose_shares
end
end
end
- 3
private
# Must be called within synchronize
- 3
def busy_for_exclusive?(purpose)
busy_for_sharing?(purpose) ||
@sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
end
- 3
def busy_for_sharing?(purpose)
(@exclusive_thread && @exclusive_thread != Thread.current) ||
@waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) }
end
- 3
def eligible_waiters?(compatible)
@waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } }
end
- 3
def wait_for(method)
@sleeping[Thread.current] = method
@cv.wait_while { yield }
ensure
@sleeping.delete Thread.current
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/concern"
- 1
require "active_support/ordered_options"
- 1
module ActiveSupport
# Configurable provides a <tt>config</tt> method to store and retrieve
# configuration options as an <tt>OrderedOptions</tt>.
- 1
module Configurable
- 1
extend ActiveSupport::Concern
- 1
class Configuration < ActiveSupport::InheritableOptions
- 1
def compile_methods!
self.class.compile_methods!(keys)
end
# Compiles reader methods so we don't have to go through method_missing.
- 1
def self.compile_methods!(keys)
keys.reject { |m| method_defined?(m) }.each do |key|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{key}; _get(#{key.inspect}); end
RUBY
end
end
end
- 1
module ClassMethods
- 1
def config
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
superclass.config.inheritable_copy
else
# create a new "anonymous" class that will host the compiled reader methods
Class.new(Configuration).new
end
end
- 1
def configure
yield config
end
# Allows you to add shortcut so that you don't have to refer to attribute
# through config. Also look at the example for config to contrast.
#
# Defines both class and instance config accessors.
#
# class User
# include ActiveSupport::Configurable
# config_accessor :allowed_access
# end
#
# User.allowed_access # => nil
# User.allowed_access = false
# User.allowed_access # => false
#
# user = User.new
# user.allowed_access # => false
# user.allowed_access = true
# user.allowed_access # => true
#
# User.allowed_access # => false
#
# The attribute name must be a valid method name in Ruby.
#
# class User
# include ActiveSupport::Configurable
# config_accessor :"1_Badname"
# end
# # => NameError: invalid config attribute name
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class User
# include ActiveSupport::Configurable
# config_accessor :allowed_access, instance_reader: false, instance_writer: false
# end
#
# User.allowed_access = false
# User.allowed_access # => false
#
# User.new.allowed_access = true # => NoMethodError
# User.new.allowed_access # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class User
# include ActiveSupport::Configurable
# config_accessor :allowed_access, instance_accessor: false
# end
#
# User.allowed_access = false
# User.allowed_access # => false
#
# User.new.allowed_access = true # => NoMethodError
# User.new.allowed_access # => NoMethodError
#
# Also you can pass a block to set up the attribute with a default value.
#
# class User
# include ActiveSupport::Configurable
# config_accessor :hair_colors do
# [:brown, :black, :blonde, :red]
# end
# end
#
# User.hair_colors # => [:brown, :black, :blonde, :red]
- 1
def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc:
- 3
names.each do |name|
- 3
raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name)
- 3
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
- 3
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
- 3
singleton_class.class_eval reader, __FILE__, reader_line
- 3
singleton_class.class_eval writer, __FILE__, writer_line
- 3
if instance_accessor
- 2
class_eval reader, __FILE__, reader_line if instance_reader
- 2
class_eval writer, __FILE__, writer_line if instance_writer
end
- 3
send("#{name}=", yield) if block_given?
end
end
- 1
private :config_accessor
end
# Reads and writes attributes from a configuration <tt>OrderedOptions</tt>.
#
# require "active_support/configurable"
#
# class User
# include ActiveSupport::Configurable
# end
#
# user = User.new
#
# user.config.allowed_access = true
# user.config.level = 1
#
# user.config.allowed_access # => true
# user.config.level # => 1
- 1
def config
@_config ||= self.class.config.inheritable_copy
end
end
end
# frozen_string_literal: true
module ActiveSupport
# Reads a YAML configuration file, evaluating any ERB, then
# parsing the resulting YAML.
#
# Warns in case of YAML confusing characters, like invisible
# non-breaking spaces.
class ConfigurationFile # :nodoc:
class FormatError < StandardError; end
def initialize(content_path)
@content_path = content_path.to_s
@content = read content_path
end
def self.parse(content_path, **options)
new(content_path).parse(**options)
end
def parse(context: nil, **options)
YAML.load(render(context), **options) || {}
rescue Psych::SyntaxError => error
raise "YAML syntax error occurred while parsing #{@content_path}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{error.message}"
end
private
def read(content_path)
require "yaml"
require "erb"
File.read(content_path).tap do |content|
if content.include?("\u00A0")
warn "File contains invisible non-breaking spaces, you may want to remove those"
end
end
end
def render(context)
erb = ERB.new(@content).tap { |e| e.filename = @content_path }
context ? erb.result(context) : erb.result
end
end
end
# frozen_string_literal: true
Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path|
require path
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/array/wrap"
- 1
require "active_support/core_ext/array/access"
- 1
require "active_support/core_ext/array/conversions"
- 1
require "active_support/core_ext/array/extract"
- 1
require "active_support/core_ext/array/extract_options"
- 1
require "active_support/core_ext/array/grouping"
- 1
require "active_support/core_ext/array/inquiry"
# frozen_string_literal: true
- 1
class Array
# Returns the tail of the array from +position+.
#
# %w( a b c d ).from(0) # => ["a", "b", "c", "d"]
# %w( a b c d ).from(2) # => ["c", "d"]
# %w( a b c d ).from(10) # => []
# %w().from(0) # => []
# %w( a b c d ).from(-2) # => ["c", "d"]
# %w( a b c ).from(-10) # => []
- 1
def from(position)
self[position, length] || []
end
# Returns the beginning of the array up to +position+.
#
# %w( a b c d ).to(0) # => ["a"]
# %w( a b c d ).to(2) # => ["a", "b", "c"]
# %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
# %w().to(0) # => []
# %w( a b c d ).to(-2) # => ["a", "b", "c"]
# %w( a b c ).to(-10) # => []
- 1
def to(position)
if position >= 0
take position + 1
else
self[0..position]
end
end
# Returns a new array that includes the passed elements.
#
# [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
# [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]
- 1
def including(*elements)
self + elements.flatten(1)
end
# Returns a copy of the Array excluding the specified elements.
#
# ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
# [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ]
#
# Note: This is an optimization of <tt>Enumerable#excluding</tt> that uses <tt>Array#-</tt>
# instead of <tt>Array#reject</tt> for performance reasons.
- 1
def excluding(*elements)
self - elements.flatten(1)
end
# Alias for #excluding.
- 1
def without(*elements)
excluding(*elements)
end
# Equal to <tt>self[1]</tt>.
#
# %w( a b c d e ).second # => "b"
- 1
def second
self[1]
end
# Equal to <tt>self[2]</tt>.
#
# %w( a b c d e ).third # => "c"
- 1
def third
self[2]
end
# Equal to <tt>self[3]</tt>.
#
# %w( a b c d e ).fourth # => "d"
- 1
def fourth
self[3]
end
# Equal to <tt>self[4]</tt>.
#
# %w( a b c d e ).fifth # => "e"
- 1
def fifth
self[4]
end
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
#
# (1..42).to_a.forty_two # => 42
- 1
def forty_two
self[41]
end
# Equal to <tt>self[-3]</tt>.
#
# %w( a b c d e ).third_to_last # => "c"
- 1
def third_to_last
self[-3]
end
# Equal to <tt>self[-2]</tt>.
#
# %w( a b c d e ).second_to_last # => "d"
- 1
def second_to_last
self[-2]
end
end
# frozen_string_literal: true
- 23
require "active_support/xml_mini"
- 23
require "active_support/core_ext/hash/keys"
- 23
require "active_support/core_ext/string/inflections"
- 23
require "active_support/core_ext/object/to_param"
- 23
require "active_support/core_ext/object/to_query"
- 23
class Array
# Converts the array to a comma-separated sentence where the last element is
# joined by the connector word.
#
# You can pass the following options to change the default behavior. If you
# pass an option key that doesn't exist in the list below, it will raise an
# <tt>ArgumentError</tt>.
#
# ==== Options
#
# * <tt>:words_connector</tt> - The sign or word used to join the elements
# in arrays with two or more elements (default: ", ").
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements
# in arrays with two elements (default: " and ").
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element
# in arrays with three or more elements (default: ", and ").
# * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
# the connector options defined on the 'support.array' namespace in the
# corresponding dictionary file.
#
# ==== Examples
#
# [].to_sentence # => ""
# ['one'].to_sentence # => "one"
# ['one', 'two'].to_sentence # => "one and two"
# ['one', 'two', 'three'].to_sentence # => "one, two, and three"
#
# ['one', 'two'].to_sentence(passing: 'invalid option')
# # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale
#
# ['one', 'two'].to_sentence(two_words_connector: '-')
# # => "one-two"
#
# ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ')
# # => "one or two or at least three"
#
# Using <tt>:locale</tt> option:
#
# # Given this locale dictionary:
# #
# # es:
# # support:
# # array:
# # words_connector: " o "
# # two_words_connector: " y "
# # last_word_connector: " o al menos "
#
# ['uno', 'dos'].to_sentence(locale: :es)
# # => "uno y dos"
#
# ['uno', 'dos', 'tres'].to_sentence(locale: :es)
# # => "uno o dos o al menos tres"
- 23
def to_sentence(options = {})
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
default_connectors = {
words_connector: ", ",
two_words_connector: " and ",
last_word_connector: ", and "
}
if defined?(I18n)
i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
default_connectors.merge!(i18n_connectors)
end
options = default_connectors.merge!(options)
case length
when 0
+""
when 1
+"#{self[0]}"
when 2
+"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
else
+"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
end
end
# 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.none.to_formatted_s(:db) # => "null"
# [1,2].to_formatted_s # => "[1, 2]"
- 23
def to_formatted_s(format = :default)
case format
when :db
if empty?
"null"
else
collect(&:id).join(",")
end
else
to_default_s
end
end
- 23
alias_method :to_default_s, :to_s
- 23
alias_method :to_s, :to_formatted_s
# Returns a string that represents the array in XML by invoking +to_xml+
# on each element. Active Record collections delegate their representation
# in XML to this method.
#
# All elements are expected to respond to +to_xml+, if any of them does
# not then an exception is raised.
#
# The root node reflects the class name of the first element in plural
# if all elements belong to the same type and that's not Hash:
#
# customer.projects.to_xml
#
# <?xml version="1.0" encoding="UTF-8"?>
# <projects type="array">
# <project>
# <amount type="decimal">20000.0</amount>
# <customer-id type="integer">1567</customer-id>
# <deal-date type="date">2008-04-09</deal-date>
# ...
# </project>
# <project>
# <amount type="decimal">57230.0</amount>
# <customer-id type="integer">1567</customer-id>
# <deal-date type="date">2008-04-15</deal-date>
# ...
# </project>
# </projects>
#
# Otherwise the root element is "objects":
#
# [{ foo: 1, bar: 2}, { baz: 3}].to_xml
#
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
# <object>
# <bar type="integer">2</bar>
# <foo type="integer">1</foo>
# </object>
# <object>
# <baz type="integer">3</baz>
# </object>
# </objects>
#
# If the collection is empty the root element is "nil-classes" by default:
#
# [].to_xml
#
# <?xml version="1.0" encoding="UTF-8"?>
# <nil-classes type="array"/>
#
# To ensure a meaningful root element use the <tt>:root</tt> option:
#
# customer_with_no_projects.projects.to_xml(root: 'projects')
#
# <?xml version="1.0" encoding="UTF-8"?>
# <projects type="array"/>
#
# By default name of the node for the children of root is <tt>root.singularize</tt>.
# You can change it with the <tt>:children</tt> option.
#
# The +options+ hash is passed downwards:
#
# Message.all.to_xml(skip_types: true)
#
# <?xml version="1.0" encoding="UTF-8"?>
# <messages>
# <message>
# <created-at>2008-03-07T09:58:18+01:00</created-at>
# <id>1</id>
# <name>1</name>
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
# <user-id>1</user-id>
# </message>
# </messages>
#
- 23
def to_xml(options = {})
require "active_support/builder" unless defined?(Builder::XmlMarkup)
options = options.dup
options[:indent] ||= 2
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
options[:root] ||= \
if first.class != Hash && all? { |e| e.is_a?(first.class) }
underscored = ActiveSupport::Inflector.underscore(first.class.name)
ActiveSupport::Inflector.pluralize(underscored).tr("/", "_")
else
"objects"
end
builder = options[:builder]
builder.instruct! unless options.delete(:skip_instruct)
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
children = options.delete(:children) || root.singularize
attributes = options[:skip_types] ? {} : { type: "array" }
if empty?
builder.tag!(root, attributes)
else
builder.tag!(root, attributes) do
each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
yield builder if block_given?
end
end
end
end
# frozen_string_literal: true
- 1
class Array
# Removes and returns the elements for which the block returns a true value.
# If no block is given, an Enumerator is returned instead.
#
# numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
# numbers # => [0, 2, 4, 6, 8]
- 1
def extract!
return to_enum(:extract!) { size } unless block_given?
extracted_elements = []
reject! do |element|
extracted_elements << element if yield(element)
end
extracted_elements
end
end
# frozen_string_literal: true
- 24
class Hash
# By default, only instances of Hash itself are extractable.
# Subclasses of Hash may implement this method and return
# true to declare themselves as extractable. If a Hash
# is extractable, Array#extract_options! pops it from
# the Array when it is the last element of the Array.
- 24
def extractable_options?
- 45
instance_of?(Hash)
end
end
- 24
class Array
# Extracts options from a set of arguments. Removes and returns the last
# element in the array if it's a hash, otherwise returns a blank hash.
#
# def options(*args)
# args.extract_options!
# end
#
# options(1, 2) # => {}
# options(1, 2, a: :b) # => {:a=>:b}
- 24
def extract_options!
- 191
if last.is_a?(Hash) && last.extractable_options?
- 45
pop
else
- 146
{}
end
end
end
# frozen_string_literal: true
- 1
class Array
# Splits or iterates over the array in groups of size +number+,
# padding any remaining slots with +fill_with+ unless it is +false+.
#
# %w(1 2 3 4 5 6 7 8 9 10).in_groups_of(3) {|group| p group}
# ["1", "2", "3"]
# ["4", "5", "6"]
# ["7", "8", "9"]
# ["10", nil, nil]
#
# %w(1 2 3 4 5).in_groups_of(2, ' ') {|group| p group}
# ["1", "2"]
# ["3", "4"]
# ["5", " "]
#
# %w(1 2 3 4 5).in_groups_of(2, false) {|group| p group}
# ["1", "2"]
# ["3", "4"]
# ["5"]
- 1
def in_groups_of(number, fill_with = nil)
if number.to_i <= 0
raise ArgumentError,
"Group size must be a positive integer, was #{number.inspect}"
end
if fill_with == false
collection = self
else
# size % number gives how many extra we have;
# subtracting from number gives how many to add;
# modulo number ensures we don't add group of just fill.
padding = (number - size % number) % number
collection = dup.concat(Array.new(padding, fill_with))
end
if block_given?
collection.each_slice(number) { |slice| yield(slice) }
else
collection.each_slice(number).to_a
end
end
# Splits or iterates over the array in +number+ of groups, padding any
# remaining slots with +fill_with+ unless it is +false+.
#
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
# ["1", "2", "3", "4"]
# ["5", "6", "7", nil]
# ["8", "9", "10", nil]
#
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3, ' ') {|group| p group}
# ["1", "2", "3", "4"]
# ["5", "6", "7", " "]
# ["8", "9", "10", " "]
#
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
# ["1", "2", "3"]
# ["4", "5"]
# ["6", "7"]
- 1
def in_groups(number, fill_with = nil)
# size.div number gives minor group size;
# size % number gives how many objects need extra accommodation;
# each group hold either division or division + 1 items.
division = size.div number
modulo = size % number
# create a new array avoiding dup
groups = []
start = 0
number.times do |index|
length = division + (modulo > 0 && modulo > index ? 1 : 0)
groups << last_group = slice(start, length)
last_group << fill_with if fill_with != false &&
modulo > 0 && length == division
start += length
end
if block_given?
groups.each { |g| yield(g) }
else
groups
end
end
# Divides the array into one or more subarrays based on a delimiting +value+
# or the result of an optional block.
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
- 1
def split(value = nil)
arr = dup
result = []
if block_given?
while (idx = arr.index { |i| yield i })
result << arr.shift(idx)
arr.shift
end
else
while (idx = arr.index(value))
result << arr.shift(idx)
arr.shift
end
end
result << arr
end
end
# frozen_string_literal: true
- 1
require "active_support/array_inquirer"
- 1
class Array
# Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
# to check its string-like contents.
#
# pets = [:cat, :dog].inquiry
#
# pets.cat? # => true
# pets.ferret? # => false
#
# pets.any?(:cat, :ferret) # => true
# pets.any?(:ferret, :alligator) # => false
- 1
def inquiry
ActiveSupport::ArrayInquirer.new(self)
end
end
# frozen_string_literal: true
require "active_support/deprecation"
ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Array#append and Array#prepend natively, so requiring active_support/core_ext/array/prepend_and_append is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
# frozen_string_literal: true
- 13
class Array
# Wraps its argument in an array unless it is already an array (or array-like).
#
# Specifically:
#
# * If the argument is +nil+ an empty array is returned.
# * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
# * Otherwise, returns an array with the argument as its single element.
#
# Array.wrap(nil) # => []
# Array.wrap([1, 2, 3]) # => [1, 2, 3]
# Array.wrap(0) # => [0]
#
# This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
#
# * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
# moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
# an array with the argument as its single element right away.
# * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
# raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
# * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+
# it returns an array with the argument as its single element.
#
# The last point is easily explained with some enumerables:
#
# Array(foo: :bar) # => [[:foo, :bar]]
# Array.wrap(foo: :bar) # => [{:foo=>:bar}]
#
# There's also a related idiom that uses the splat operator:
#
# [*object]
#
# which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise.
#
# The differences with <tt>Kernel#Array</tt> explained above
# apply to the rest of <tt>object</tt>s.
- 13
def self.wrap(object)
if object.nil?
[]
elsif object.respond_to?(:to_ary)
object.to_ary || [object]
else
[object]
end
end
end
# frozen_string_literal: true
- 2
require "benchmark"
- 2
class << Benchmark
# Benchmark realtime in milliseconds.
#
# Benchmark.realtime { User.all }
# # => 8.0e-05
#
# Benchmark.ms { User.all }
# # => 0.074
- 2
def ms(&block)
1000 * realtime(&block)
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/big_decimal/conversions"
# frozen_string_literal: true
- 2
require "bigdecimal"
- 2
require "bigdecimal/util"
- 2
module ActiveSupport
- 2
module BigDecimalWithDefaultFormat #:nodoc:
- 2
def to_s(format = "F")
- 1
super(format)
end
end
end
- 2
BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat)
# frozen_string_literal: true
- 1
require "active_support/core_ext/class/attribute"
- 1
require "active_support/core_ext/class/subclasses"
# frozen_string_literal: true
- 23
require "active_support/core_ext/module/redefine_method"
- 23
class Class
# Declare a class-level attribute whose value is inheritable by subclasses.
# Subclasses can change their own value and it will not impact parent class.
#
# ==== Options
#
# * <tt>:instance_reader</tt> - Sets the instance reader method (defaults to true).
# * <tt>:instance_writer</tt> - Sets the instance writer method (defaults to true).
# * <tt>:instance_accessor</tt> - Sets both instance methods (defaults to true).
# * <tt>:instance_predicate</tt> - Sets a predicate method (defaults to true).
# * <tt>:default</tt> - Sets a default value for the attribute (defaults to nil).
#
# ==== Examples
#
# class Base
# class_attribute :setting
# end
#
# class Subclass < Base
# end
#
# Base.setting = true
# Subclass.setting # => true
# Subclass.setting = false
# Subclass.setting # => false
# Base.setting # => true
#
# In the above case as long as Subclass does not assign a value to setting
# by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
# would read value assigned to parent class. Once Subclass assigns a value then
# the value assigned by Subclass would be returned.
#
# This matches normal Ruby method inheritance: think of writing an attribute
# on a subclass as overriding the reader method. However, you need to be aware
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
# In such cases, you don't want to do changes in place. Instead use setters:
#
# Base.setting = []
# Base.setting # => []
# Subclass.setting # => []
#
# # Appending in child changes both parent and child because it is the same object:
# Subclass.setting << :foo
# Base.setting # => [:foo]
# Subclass.setting # => [:foo]
#
# # Use setters to not propagate changes:
# Base.setting = []
# Subclass.setting += [:foo]
# Base.setting # => []
# Subclass.setting # => [:foo]
#
# For convenience, an instance predicate method is defined as well.
# To skip it, pass <tt>instance_predicate: false</tt>.
#
# Subclass.setting? # => false
#
# Instances may overwrite the class value in the same way:
#
# Base.setting = true
# object = Base.new
# object.setting # => true
# object.setting = false
# object.setting # => false
# Base.setting # => true
#
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
#
# object.setting # => NoMethodError
# object.setting? # => NoMethodError
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
#
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
#
# To set a default value for the attribute, pass <tt>default:</tt>, like so:
#
# class_attribute :settings, default: {}
- 23
def class_attribute(*attrs, instance_accessor: true,
instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
- 113
class_methods, methods = [], []
- 113
attrs.each do |name|
- 113
unless name.is_a?(Symbol) || name.is_a?(String)
raise TypeError, "#{name.inspect} is not a symbol nor a string"
end
- 113
class_methods << <<~RUBY # In case the method exists and is not public
silence_redefinition_of_method def #{name}
end
RUBY
- 113
methods << <<~RUBY if instance_reader
silence_redefinition_of_method def #{name}
defined?(@#{name}) ? @#{name} : self.class.#{name}
end
RUBY
- 113
class_methods << <<~RUBY
silence_redefinition_of_method def #{name}=(value)
redefine_method(:#{name}) { value } if singleton_class?
redefine_singleton_method(:#{name}) { value }
value
end
RUBY
- 113
methods << <<~RUBY if instance_writer
silence_redefinition_of_method(:#{name}=)
attr_writer :#{name}
RUBY
- 113
if instance_predicate
- 113
class_methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
- 113
if instance_reader
- 113
methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
end
end
end
- 113
location = caller_locations(1, 1).first
- 113
class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
- 226
attrs.each { |name| public_send("#{name}=", default) }
end
end
# frozen_string_literal: true
# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
# but we keep this around for libraries that directly require it knowing they
# want cattr_*. No need to deprecate.
require "active_support/core_ext/module/attribute_accessors"
# frozen_string_literal: true
- 1
class Class
# Returns an array with all classes that are < than its receiver.
#
# class C; end
# C.descendants # => []
#
# class B < C; end
# C.descendants # => [B]
#
# class A < B; end
# C.descendants # => [B, A]
#
# class D < C; end
# C.descendants # => [B, A, D]
- 1
def descendants
ObjectSpace.each_object(singleton_class).reject do |k|
k.singleton_class? || k == self
end
end
# Returns an array with the direct children of +self+.
#
# class Foo; end
# class Bar < Foo; end
# class Baz < Bar; end
#
# Foo.subclasses # => [Bar]
- 1
def subclasses
descendants.select { |descendant| descendant.superclass == self }
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/date/acts_like"
- 2
require "active_support/core_ext/date/blank"
- 2
require "active_support/core_ext/date/calculations"
- 2
require "active_support/core_ext/date/conversions"
- 2
require "active_support/core_ext/date/zones"
# frozen_string_literal: true
- 14
require "active_support/core_ext/object/acts_like"
- 14
class Date
# Duck-types as a Date-like class. See Object#acts_like?.
- 14
def acts_like_date?
true
end
end
# frozen_string_literal: true
- 2
require "date"
- 2
class Date #:nodoc:
# No Date is blank:
#
# Date.today.blank? # => false
#
# @return [false]
- 2
def blank?
false
end
end
# frozen_string_literal: true
- 23
require "date"
- 23
require "active_support/duration"
- 23
require "active_support/core_ext/object/acts_like"
- 23
require "active_support/core_ext/date/zones"
- 23
require "active_support/core_ext/time/zones"
- 23
require "active_support/core_ext/date_and_time/calculations"
- 23
class Date
- 23
include DateAndTime::Calculations
- 23
class << self
- 23
attr_accessor :beginning_of_week_default
# Returns the week start (e.g. :monday) for the current request, if this has been set (via Date.beginning_of_week=).
# If <tt>Date.beginning_of_week</tt> has not been set for the current request, returns the week start specified in <tt>config.beginning_of_week</tt>.
# If no config.beginning_of_week was specified, returns :monday.
- 23
def beginning_of_week
Thread.current[:beginning_of_week] || beginning_of_week_default || :monday
end
# Sets <tt>Date.beginning_of_week</tt> to a week start (e.g. :monday) for current request/thread.
#
# This method accepts any of the following day symbols:
# :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
- 23
def beginning_of_week=(week_start)
Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
end
# Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.
- 23
def find_beginning_of_week!(week_start)
raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start)
week_start
end
# Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
- 23
def yesterday
::Date.current.yesterday
end
# Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
- 23
def tomorrow
::Date.current.tomorrow
end
# Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
- 23
def current
::Time.zone ? ::Time.zone.today : ::Date.today
end
end
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
# and then subtracts the specified number of seconds.
- 23
def ago(seconds)
in_time_zone.since(-seconds)
end
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
# and then adds the specified number of seconds
- 23
def since(seconds)
in_time_zone.since(seconds)
end
- 23
alias :in :since
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
- 23
def beginning_of_day
in_time_zone
end
- 23
alias :midnight :beginning_of_day
- 23
alias :at_midnight :beginning_of_day
- 23
alias :at_beginning_of_day :beginning_of_day
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00)
- 23
def middle_of_day
in_time_zone.middle_of_day
end
- 23
alias :midday :middle_of_day
- 23
alias :noon :middle_of_day
- 23
alias :at_midday :middle_of_day
- 23
alias :at_noon :middle_of_day
- 23
alias :at_middle_of_day :middle_of_day
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
- 23
def end_of_day
in_time_zone.end_of_day
end
- 23
alias :at_end_of_day :end_of_day
- 23
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
- 23
alias_method :plus_without_duration, :+
- 23
alias_method :+, :plus_with_duration
- 23
def minus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
plus_with_duration(-other)
else
minus_without_duration(other)
end
end
- 23
alias_method :minus_without_duration, :-
- 23
alias_method :-, :minus_with_duration
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
# any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
- 23
def advance(options)
d = self
d = d >> options[:years] * 12 if options[:years]
d = d >> options[:months] if options[:months]
d = d + options[:weeks] * 7 if options[:weeks]
d = d + options[:days] if options[:days]
d
end
# Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
# The +options+ parameter is a hash with a combination of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>.
#
# Date.new(2007, 5, 12).change(day: 1) # => Date.new(2007, 5, 1)
# Date.new(2007, 5, 12).change(year: 2005, month: 1) # => Date.new(2005, 1, 12)
- 23
def change(options)
::Date.new(
options.fetch(:year, year),
options.fetch(:month, month),
options.fetch(:day, day)
)
end
# Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
- 23
def compare_with_coercion(other)
if other.is_a?(Time)
to_datetime <=> other
else
compare_without_coercion(other)
end
end
- 23
alias_method :compare_without_coercion, :<=>
- 23
alias_method :<=>, :compare_with_coercion
end
# frozen_string_literal: true
- 2
require "date"
- 2
require "active_support/inflector/methods"
- 2
require "active_support/core_ext/date/zones"
- 2
require "active_support/core_ext/module/redefine_method"
- 2
class Date
- 2
DATE_FORMATS = {
short: "%d %b",
long: "%B %d, %Y",
db: "%Y-%m-%d",
inspect: "%Y-%m-%d",
number: "%Y%m%d",
long_ordinal: lambda { |date|
day_format = ActiveSupport::Inflector.ordinalize(date.day)
date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
},
rfc822: "%d %b %Y",
iso8601: lambda { |date| date.iso8601 }
}
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
#
# This method is aliased to <tt>to_s</tt>.
#
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
#
# date.to_formatted_s(:db) # => "2007-11-10"
# date.to_s(:db) # => "2007-11-10"
#
# date.to_formatted_s(:short) # => "10 Nov"
# date.to_formatted_s(:number) # => "20071110"
# date.to_formatted_s(:long) # => "November 10, 2007"
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
# date.to_formatted_s(:iso8601) # => "2007-11-10"
#
# == Adding your own date formats to to_formatted_s
# You can add your own formats to the Date::DATE_FORMATS hash.
# Use the format name as the hash key and either a strftime string
# or Proc instance that takes a date argument as the value.
#
# # config/initializers/date_formats.rb
# Date::DATE_FORMATS[:month_and_year] = '%B %Y'
# Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
- 2
def to_formatted_s(format = :default)
if formatter = DATE_FORMATS[format]
if formatter.respond_to?(:call)
formatter.call(self).to_s
else
strftime(formatter)
end
else
to_default_s
end
end
- 2
alias_method :to_default_s, :to_s
- 2
alias_method :to_s, :to_formatted_s
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
- 2
def readable_inspect
strftime("%a, %d %b %Y")
end
- 2
alias_method :default_inspect, :inspect
- 2
alias_method :inspect, :readable_inspect
- 2
silence_redefinition_of_method :to_time
# Converts a Date instance to a Time, where the time is set to the beginning of the day.
# The timezone can be either :local or :utc (default :local).
#
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 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) # => 2007-11-10 00:00:00 UTC
#
# NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ'].
# If the *application's* timezone is needed, then use +in_time_zone+ instead.
- 2
def to_time(form = :local)
raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form)
::Time.send(form, year, month, day)
end
- 2
silence_redefinition_of_method :xmlschema
# Returns a string which represents the time in used time zone as DateTime
# defined by XML Schema:
#
# date = Date.new(2015, 05, 23) # => Sat, 23 May 2015
# date.xmlschema # => "2015-05-23T00:00:00+04:00"
- 2
def xmlschema
in_time_zone.xmlschema
end
end
# frozen_string_literal: true
- 23
require "date"
- 23
require "active_support/core_ext/date_and_time/zones"
- 23
class Date
- 23
include DateAndTime::Zones
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/object/try"
- 23
require "active_support/core_ext/date_time/conversions"
- 23
module DateAndTime
- 23
module Calculations
- 23
DAYS_INTO_WEEK = {
sunday: 0,
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6
}
- 23
WEEKEND_DAYS = [ 6, 0 ]
# Returns a new date/time representing yesterday.
- 23
def yesterday
advance(days: -1)
end
# Returns a new date/time representing tomorrow.
- 23
def tomorrow
advance(days: 1)
end
# Returns true if the date/time is today.
- 23
def today?
to_date == ::Date.current
end
# Returns true if the date/time is tomorrow.
- 23
def tomorrow?
to_date == ::Date.current.tomorrow
end
- 23
alias :next_day? :tomorrow?
# Returns true if the date/time is yesterday.
- 23
def yesterday?
to_date == ::Date.current.yesterday
end
- 23
alias :prev_day? :yesterday?
# Returns true if the date/time is in the past.
- 23
def past?
self < self.class.current
end
# Returns true if the date/time is in the future.
- 23
def future?
self > self.class.current
end
# Returns true if the date/time falls on a Saturday or Sunday.
- 23
def on_weekend?
WEEKEND_DAYS.include?(wday)
end
# Returns true if the date/time does not fall on a Saturday or Sunday.
- 23
def on_weekday?
!WEEKEND_DAYS.include?(wday)
end
# Returns true if the date/time falls before <tt>date_or_time</tt>.
- 23
def before?(date_or_time)
self < date_or_time
end
# Returns true if the date/time falls after <tt>date_or_time</tt>.
- 23
def after?(date_or_time)
self > date_or_time
end
# Returns a new date/time the specified number of days ago.
- 23
def days_ago(days)
advance(days: -days)
end
# Returns a new date/time the specified number of days in the future.
- 23
def days_since(days)
advance(days: days)
end
# Returns a new date/time the specified number of weeks ago.
- 23
def weeks_ago(weeks)
advance(weeks: -weeks)
end
# Returns a new date/time the specified number of weeks in the future.
- 23
def weeks_since(weeks)
advance(weeks: weeks)
end
# Returns a new date/time the specified number of months ago.
- 23
def months_ago(months)
advance(months: -months)
end
# Returns a new date/time the specified number of months in the future.
- 23
def months_since(months)
advance(months: months)
end
# Returns a new date/time the specified number of years ago.
- 23
def years_ago(years)
advance(years: -years)
end
# Returns a new date/time the specified number of years in the future.
- 23
def years_since(years)
advance(years: years)
end
# Returns a new date/time at the start of the month.
#
# today = Date.today # => Thu, 18 Jun 2015
# today.beginning_of_month # => Mon, 01 Jun 2015
#
# +DateTime+ objects will have a time set to 0:00.
#
# now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000
# now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000
- 23
def beginning_of_month
first_hour(change(day: 1))
end
- 23
alias :at_beginning_of_month :beginning_of_month
# Returns a new date/time at the start of the quarter.
#
# today = Date.today # => Fri, 10 Jul 2015
# today.beginning_of_quarter # => Wed, 01 Jul 2015
#
# +DateTime+ objects will have a time set to 0:00.
#
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
# now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000
- 23
def beginning_of_quarter
first_quarter_month = month - (2 + month) % 3
beginning_of_month.change(month: first_quarter_month)
end
- 23
alias :at_beginning_of_quarter :beginning_of_quarter
# Returns a new date/time at the end of the quarter.
#
# today = Date.today # => Fri, 10 Jul 2015
# today.end_of_quarter # => Wed, 30 Sep 2015
#
# +DateTime+ objects will have a time set to 23:59:59.
#
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
# now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000
- 23
def end_of_quarter
last_quarter_month = month + (12 - month) % 3
beginning_of_month.change(month: last_quarter_month).end_of_month
end
- 23
alias :at_end_of_quarter :end_of_quarter
# Returns a new date/time at the beginning of the year.
#
# today = Date.today # => Fri, 10 Jul 2015
# today.beginning_of_year # => Thu, 01 Jan 2015
#
# +DateTime+ objects will have a time set to 0:00.
#
# now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000
# now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000
- 23
def beginning_of_year
change(month: 1).beginning_of_month
end
- 23
alias :at_beginning_of_year :beginning_of_year
# Returns a new date/time representing the given day in the next week.
#
# today = Date.today # => Thu, 07 May 2015
# today.next_week # => Mon, 11 May 2015
#
# The +given_day_in_next_week+ defaults to the beginning of the week
# which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
# when set.
#
# today = Date.today # => Thu, 07 May 2015
# today.next_week(:friday) # => Fri, 15 May 2015
#
# +DateTime+ objects have their time set to 0:00 unless +same_time+ is true.
#
# now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000
# now.next_week # => Mon, 11 May 2015 00:00:00 +0000
- 23
def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false)
result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
same_time ? copy_time_to(result) : result
end
# Returns a new date/time representing the next weekday.
- 23
def next_weekday
if next_day.on_weekend?
next_week(:monday, same_time: true)
else
next_day
end
end
# Short-hand for months_since(3)
- 23
def next_quarter
months_since(3)
end
# Returns a new date/time representing the given day in the previous week.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 0:00 unless +same_time+ is true.
- 23
def prev_week(start_day = Date.beginning_of_week, same_time: false)
result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
same_time ? copy_time_to(result) : result
end
- 23
alias_method :last_week, :prev_week
# Returns a new date/time representing the previous weekday.
- 23
def prev_weekday
if prev_day.on_weekend?
copy_time_to(beginning_of_week(:friday))
else
prev_day
end
end
- 23
alias_method :last_weekday, :prev_weekday
# Short-hand for months_ago(1).
- 23
def last_month
months_ago(1)
end
# Short-hand for months_ago(3).
- 23
def prev_quarter
months_ago(3)
end
- 23
alias_method :last_quarter, :prev_quarter
# Short-hand for years_ago(1).
- 23
def last_year
years_ago(1)
end
# Returns the number of days to the start of the week on the given day.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
- 23
def days_to_week_start(start_day = Date.beginning_of_week)
start_day_number = DAYS_INTO_WEEK.fetch(start_day)
(wday - start_day_number) % 7
end
# Returns a new date/time representing the start of this week on the given day.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# +DateTime+ objects have their time set to 0:00.
- 23
def beginning_of_week(start_day = Date.beginning_of_week)
result = days_ago(days_to_week_start(start_day))
acts_like?(:time) ? result.midnight : result
end
- 23
alias :at_beginning_of_week :beginning_of_week
# Returns Monday of this week assuming that week starts on Monday.
# +DateTime+ objects have their time set to 0:00.
- 23
def monday
beginning_of_week(:monday)
end
# Returns a new date/time representing the end of this week on the given day.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 23:59:59.
- 23
def end_of_week(start_day = Date.beginning_of_week)
last_hour(days_since(6 - days_to_week_start(start_day)))
end
- 23
alias :at_end_of_week :end_of_week
# Returns Sunday of this week assuming that week starts on Monday.
# +DateTime+ objects have their time set to 23:59:59.
- 23
def sunday
end_of_week(:monday)
end
# Returns a new date/time representing the end of the month.
# DateTime objects will have a time set to 23:59:59.
- 23
def end_of_month
last_day = ::Time.days_in_month(month, year)
last_hour(days_since(last_day - day))
end
- 23
alias :at_end_of_month :end_of_month
# Returns a new date/time representing the end of the year.
# DateTime objects will have a time set to 23:59:59.
- 23
def end_of_year
change(month: 12).end_of_month
end
- 23
alias :at_end_of_year :end_of_year
# Returns a Range representing the whole day of the current date/time.
- 23
def all_day
beginning_of_day..end_of_day
end
# Returns a Range representing the whole week of the current date/time.
# Week starts on start_day, default is <tt>Date.beginning_of_week</tt> or <tt>config.beginning_of_week</tt> when set.
- 23
def all_week(start_day = Date.beginning_of_week)
beginning_of_week(start_day)..end_of_week(start_day)
end
# Returns a Range representing the whole month of the current date/time.
- 23
def all_month
beginning_of_month..end_of_month
end
# Returns a Range representing the whole quarter of the current date/time.
- 23
def all_quarter
beginning_of_quarter..end_of_quarter
end
# Returns a Range representing the whole year of the current date/time.
- 23
def all_year
beginning_of_year..end_of_year
end
# Returns a new date/time representing the next occurrence of the specified day of week.
#
# today = Date.today # => Thu, 14 Dec 2017
# today.next_occurring(:monday) # => Mon, 18 Dec 2017
# today.next_occurring(:thursday) # => Thu, 21 Dec 2017
- 23
def next_occurring(day_of_week)
from_now = DAYS_INTO_WEEK.fetch(day_of_week) - wday
from_now += 7 unless from_now > 0
advance(days: from_now)
end
# Returns a new date/time representing the previous occurrence of the specified day of week.
#
# today = Date.today # => Thu, 14 Dec 2017
# today.prev_occurring(:monday) # => Mon, 11 Dec 2017
# today.prev_occurring(:thursday) # => Thu, 07 Dec 2017
- 23
def prev_occurring(day_of_week)
ago = wday - DAYS_INTO_WEEK.fetch(day_of_week)
ago += 7 unless ago > 0
advance(days: -ago)
end
- 23
private
- 23
def first_hour(date_or_time)
date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
end
- 23
def last_hour(date_or_time)
date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
end
- 23
def days_span(day)
(DAYS_INTO_WEEK.fetch(day) - DAYS_INTO_WEEK.fetch(Date.beginning_of_week)) % 7
end
- 23
def copy_time_to(other)
other.change(hour: hour, min: min, sec: sec, nsec: try(:nsec))
end
end
end
# frozen_string_literal: true
- 24
require "active_support/core_ext/module/attribute_accessors"
- 24
module DateAndTime
- 24
module Compatibility
# If true, +to_time+ preserves the timezone offset of receiver.
#
# NOTE: With Ruby 2.4+ the default for +to_time+ changed from
# converting to the local system time, to preserving the offset
# of the receiver. For backwards compatibility we're overriding
# this behavior, but new apps will have an initializer that sets
# this to true, because the new behavior is preferred.
- 24
mattr_accessor :preserve_timezone, instance_writer: false, default: false
# Change the output of <tt>ActiveSupport::TimeZone.utc_to_local</tt>.
#
# When `true`, it returns local times with an UTC offset, with `false` local
# times are returned as UTC.
#
# # Given this zone:
# zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
#
# # With `utc_to_local_returns_utc_offset_times = false`, local time is converted to UTC:
# zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 UTC
#
# # With `utc_to_local_returns_utc_offset_times = true`, local time is returned with UTC offset:
# zone.utc_to_local(Time.utc(2000, 1)) # => 1999-12-31 19:00:00 -0500
- 24
mattr_accessor :utc_to_local_returns_utc_offset_times, instance_writer: false, default: false
end
end
# frozen_string_literal: true
- 23
module DateAndTime
- 23
module Zones
# Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
# if Time.zone_default is set. Otherwise, it returns the current time.
#
# Time.zone = 'Hawaii' # => 'Hawaii'
# Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
#
# This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
# instead of the operating system's time zone.
#
# You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
# and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
#
# Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
# Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
- 23
def in_time_zone(zone = ::Time.zone)
time_zone = ::Time.find_zone! zone
time = acts_like?(:time) ? self : nil
if time_zone
time_with_zone(time, time_zone)
else
time || to_time
end
end
- 23
private
- 23
def time_with_zone(time, zone)
if time
ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
else
ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
end
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/date_time/acts_like"
- 2
require "active_support/core_ext/date_time/blank"
- 2
require "active_support/core_ext/date_time/calculations"
- 2
require "active_support/core_ext/date_time/compatibility"
- 2
require "active_support/core_ext/date_time/conversions"
# frozen_string_literal: true
- 2
require "date"
- 2
require "active_support/core_ext/object/acts_like"
- 2
class DateTime
# Duck-types as a Date-like class. See Object#acts_like?.
- 2
def acts_like_date?
true
end
# Duck-types as a Time-like class. See Object#acts_like?.
- 2
def acts_like_time?
true
end
end
# frozen_string_literal: true
- 2
require "date"
- 2
class DateTime #:nodoc:
# No DateTime is ever blank:
#
# DateTime.now.blank? # => false
#
# @return [false]
- 2
def blank?
false
end
end
# frozen_string_literal: true
- 23
require "date"
- 23
class DateTime
- 23
class << self
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
# <tt>config.time_zone</tt> are set, otherwise returns
# <tt>Time.now.to_datetime</tt>.
- 23
def current
::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
end
end
# Returns the number of seconds since 00:00:00.
#
# DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0
# DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
# DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
- 23
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
end
# Returns the number of seconds until 23:59:59.
#
# DateTime.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
# DateTime.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
# DateTime.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
- 23
def seconds_until_end_of_day
end_of_day.to_i - to_i
end
# Returns the fraction of a second as a +Rational+
#
# DateTime.new(2012, 8, 29, 0, 0, 0.5).subsec # => (1/2)
- 23
def subsec
sec_fraction
end
# Returns a new DateTime where one or more of the elements have been changed
# according to the +options+ parameter. The time options (<tt>:hour</tt>,
# <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
# passed, then minute and sec is set to 0. If the hour and minute is passed,
# then sec is set to 0. The +options+ parameter takes a hash with any of these
# keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>,
# <tt>:min</tt>, <tt>:sec</tt>, <tt>:offset</tt>, <tt>:start</tt>.
#
# DateTime.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => DateTime.new(2012, 8, 1, 22, 35, 0)
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => DateTime.new(1981, 8, 1, 22, 35, 0)
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
- 23
def change(options)
if new_nsec = options[:nsec]
raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
new_fraction = Rational(new_nsec, 1000000000)
else
new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
new_fraction = Rational(new_usec, 1000000)
end
raise ArgumentError, "argument out of range" if new_fraction >= 1
::DateTime.civil(
options.fetch(:year, year),
options.fetch(:month, month),
options.fetch(:day, day),
options.fetch(:hour, hour),
options.fetch(:min, options[:hour] ? 0 : min),
options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
options.fetch(:offset, offset),
options.fetch(:start, start)
)
end
# Uses Date to provide precise Time calculations for years, months, and days.
# The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
# <tt>:minutes</tt>, <tt>:seconds</tt>.
- 23
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
end
unless options[:days].nil?
options[:days], partial_days = options[:days].divmod(1)
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
d = to_date.advance(options)
datetime_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
options.fetch(:hours, 0) * 3600
if seconds_to_advance.zero?
datetime_advanced_by_date
else
datetime_advanced_by_date.since(seconds_to_advance)
end
end
# Returns a new DateTime representing the time a number of seconds ago.
# Do not use this method in combination with x.months, use months_ago instead!
- 23
def ago(seconds)
since(-seconds)
end
# Returns a new DateTime representing the time a number of seconds since the
# instance time. Do not use this method in combination with x.months, use
# months_since instead!
- 23
def since(seconds)
self + Rational(seconds, 86400)
end
- 23
alias :in :since
# Returns a new DateTime representing the start of the day (0:00).
- 23
def beginning_of_day
change(hour: 0)
end
- 23
alias :midnight :beginning_of_day
- 23
alias :at_midnight :beginning_of_day
- 23
alias :at_beginning_of_day :beginning_of_day
# Returns a new DateTime representing the middle of the day (12:00)
- 23
def middle_of_day
change(hour: 12)
end
- 23
alias :midday :middle_of_day
- 23
alias :noon :middle_of_day
- 23
alias :at_midday :middle_of_day
- 23
alias :at_noon :middle_of_day
- 23
alias :at_middle_of_day :middle_of_day
# Returns a new DateTime representing the end of the day (23:59:59).
- 23
def end_of_day
change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
end
- 23
alias :at_end_of_day :end_of_day
# Returns a new DateTime representing the start of the hour (hh:00:00).
- 23
def beginning_of_hour
change(min: 0)
end
- 23
alias :at_beginning_of_hour :beginning_of_hour
# Returns a new DateTime representing the end of the hour (hh:59:59).
- 23
def end_of_hour
change(min: 59, sec: 59, usec: Rational(999999999, 1000))
end
- 23
alias :at_end_of_hour :end_of_hour
# Returns a new DateTime representing the start of the minute (hh:mm:00).
- 23
def beginning_of_minute
change(sec: 0)
end
- 23
alias :at_beginning_of_minute :beginning_of_minute
# Returns a new DateTime representing the end of the minute (hh:mm:59).
- 23
def end_of_minute
change(sec: 59, usec: Rational(999999999, 1000))
end
- 23
alias :at_end_of_minute :end_of_minute
# Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
- 23
def localtime(utc_offset = nil)
utc = new_offset(0)
Time.utc(
utc.year, utc.month, utc.day,
utc.hour, utc.min, utc.sec + utc.sec_fraction
).getlocal(utc_offset)
end
- 23
alias_method :getlocal, :localtime
# Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
#
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 UTC
- 23
def utc
utc = new_offset(0)
Time.utc(
utc.year, utc.month, utc.day,
utc.hour, utc.min, utc.sec + utc.sec_fraction
)
end
- 23
alias_method :getgm, :utc
- 23
alias_method :getutc, :utc
- 23
alias_method :gmtime, :utc
# Returns +true+ if <tt>offset == 0</tt>.
- 23
def utc?
offset == 0
end
# Returns the offset value in seconds.
- 23
def utc_offset
(offset * 86400).to_i
end
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
- 23
def <=>(other)
if other.respond_to? :to_datetime
super other.to_datetime rescue nil
else
super
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/date_and_time/compatibility"
- 2
require "active_support/core_ext/module/redefine_method"
- 2
class DateTime
- 2
include DateAndTime::Compatibility
- 2
silence_redefinition_of_method :to_time
# Either return an instance of +Time+ with the same UTC offset
# as +self+ or an instance of +Time+ representing the same time
# in the local system timezone depending on the setting of
# on the setting of +ActiveSupport.to_time_preserves_timezone+.
- 2
def to_time
preserve_timezone ? getlocal(utc_offset) : getlocal
end
end
# frozen_string_literal: true
- 23
require "date"
- 23
require "active_support/inflector/methods"
- 23
require "active_support/core_ext/time/conversions"
- 23
require "active_support/core_ext/date_time/calculations"
- 23
require "active_support/values/time_zone"
- 23
class DateTime
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
#
# This method is aliased to <tt>to_s</tt>.
#
# === Examples
# datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
#
# datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
# datetime.to_s(:db) # => "2007-12-04 00:00:00"
# datetime.to_s(:number) # => "20071204000000"
# datetime.to_formatted_s(:short) # => "04 Dec 00:00"
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
# datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00"
#
# == Adding your own datetime formats to to_formatted_s
# DateTime formats are shared with Time. You can add your own to the
# Time::DATE_FORMATS hash. Use the format name as the hash key and
# either a strftime string or Proc instance that takes a time or
# datetime argument as the value.
#
# # config/initializers/time_formats.rb
# Time::DATE_FORMATS[:month_and_year] = '%B %Y'
# Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
- 23
def to_formatted_s(format = :default)
if formatter = ::Time::DATE_FORMATS[format]
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
else
to_default_s
end
end
- 23
alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s)
- 23
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"
# datetime.formatted_offset(false) # => "-0600"
- 23
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005 14:30:00 +0000".
- 23
def readable_inspect
to_s(:rfc822)
end
- 23
alias_method :default_inspect, :inspect
- 23
alias_method :inspect, :readable_inspect
# Returns DateTime with local offset for given year if format is local else
# offset is zero.
#
# DateTime.civil_from_format :local, 2012
# # => Sun, 01 Jan 2012 00:00:00 +0300
# DateTime.civil_from_format :local, 2012, 12, 17
# # => Mon, 17 Dec 2012 00:00:00 +0000
- 23
def self.civil_from_format(utc_or_local, year, month = 1, day = 1, hour = 0, min = 0, sec = 0)
if utc_or_local.to_sym == :local
offset = ::Time.local(year, month, day).utc_offset.to_r / 86400
else
offset = 0
end
civil(year, month, day, hour, min, sec, offset)
end
# Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
- 23
def to_f
seconds_since_unix_epoch.to_f + sec_fraction
end
# Converts +self+ to an integer number of seconds since the Unix epoch.
- 23
def to_i
seconds_since_unix_epoch.to_i
end
# Returns the fraction of a second as microseconds
- 23
def usec
(sec_fraction * 1_000_000).to_i
end
# Returns the fraction of a second as nanoseconds
- 23
def nsec
(sec_fraction * 1_000_000_000).to_i
end
- 23
private
- 23
def offset_in_seconds
(offset * 86400).to_i
end
- 23
def seconds_since_unix_epoch
(jd - 2440588) * 86400 - offset_in_seconds + seconds_since_midnight
end
end
# frozen_string_literal: true
require "active_support/core_ext/digest/uuid"
# frozen_string_literal: true
- 1
require "securerandom"
- 1
module Digest
- 1
module UUID
- 1
DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
- 1
URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
- 1
OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
- 1
X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
# Generates a v5 non-random UUID (Universally Unique IDentifier).
#
# Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
# uuid_from_hash always generates the same UUID for a given name and namespace combination.
#
# See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
- 1
def self.uuid_from_hash(hash_class, uuid_namespace, name)
if hash_class == Digest::MD5
version = 3
elsif hash_class == Digest::SHA1
version = 5
else
raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
end
hash = hash_class.new
hash.update(uuid_namespace)
hash.update(name)
ary = hash.digest.unpack("NnnnnN")
ary[2] = (ary[2] & 0x0FFF) | (version << 12)
ary[3] = (ary[3] & 0x3FFF) | 0x8000
"%08x-%04x-%04x-%04x-%04x%08x" % ary
end
# Convenience method for uuid_from_hash using Digest::MD5.
- 1
def self.uuid_v3(uuid_namespace, name)
uuid_from_hash(Digest::MD5, uuid_namespace, name)
end
# Convenience method for uuid_from_hash using Digest::SHA1.
- 1
def self.uuid_v5(uuid_namespace, name)
uuid_from_hash(Digest::SHA1, uuid_namespace, name)
end
# Convenience method for SecureRandom.uuid.
- 1
def self.uuid_v4
SecureRandom.uuid
end
end
end
# frozen_string_literal: true
- 23
module Enumerable
- 23
INDEX_WITH_DEFAULT = Object.new
- 23
private_constant :INDEX_WITH_DEFAULT
# Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
# when we omit an identity.
# :stopdoc:
# We can't use Refinements here because Refinements with Module which will be prepended
# doesn't work well https://bugs.ruby-lang.org/issues/13446
- 23
alias :_original_sum_with_required_identity :sum
- 23
private :_original_sum_with_required_identity
# :startdoc:
# Calculates a sum from the elements.
#
# payments.sum { |p| p.price * p.tax_rate }
# payments.sum(&:price)
#
# The latter is a shortcut for:
#
# payments.inject(0) { |sum, p| sum + p.price }
#
# It can also calculate the sum without the use of a block.
#
# [5, 15, 10].sum # => 30
# ['foo', 'bar'].sum # => "foobar"
# [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
#
# The default sum of an empty list is zero. You can override this default:
#
# [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
- 23
def sum(identity = nil, &block)
if identity
_original_sum_with_required_identity(identity, &block)
elsif block_given?
map(&block).sum(identity)
else
inject(:+) || 0
end
end
# Convert an enumerable to a hash, using the block result as the key and the
# element as the value.
#
# people.index_by(&:login)
# # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
#
# people.index_by { |person| "#{person.first_name} #{person.last_name}" }
# # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
- 23
def index_by
if block_given?
result = {}
each { |elem| result[yield(elem)] = elem }
result
else
to_enum(:index_by) { size if respond_to?(:size) }
end
end
# Convert an enumerable to a hash, using the element as the key and the block
# result as the value.
#
# post = Post.new(title: "hey there", body: "what's up?")
#
# %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
# # => { title: "hey there", body: "what's up?" }
#
# If an argument is passed instead of a block, it will be used as the value
# for all elements:
#
# %i( created_at updated_at ).index_with(Time.now)
# # => { created_at: 2020-03-09 22:31:47, updated_at: 2020-03-09 22:31:47 }
- 23
def index_with(default = INDEX_WITH_DEFAULT)
if block_given?
result = {}
each { |elem| result[elem] = yield(elem) }
result
elsif default != INDEX_WITH_DEFAULT
result = {}
each { |elem| result[elem] = default }
result
else
to_enum(:index_with) { size if respond_to?(:size) }
end
end
# Returns +true+ if the enumerable has more than 1 element. Functionally
# equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
# much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
# if more than one person is over 26.
- 23
def many?
cnt = 0
if block_given?
any? do |element|
cnt += 1 if yield element
cnt > 1
end
else
any? { (cnt += 1) > 1 }
end
end
# Returns a new array that includes the passed elements.
#
# [ 1, 2, 3 ].including(4, 5)
# # => [ 1, 2, 3, 4, 5 ]
#
# ["David", "Rafael"].including %w[ Aaron Todd ]
# # => ["David", "Rafael", "Aaron", "Todd"]
- 23
def including(*elements)
to_a.including(*elements)
end
# The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
# collection does not include the object.
- 23
def exclude?(object)
!include?(object)
end
# Returns a copy of the enumerable excluding the specified elements.
#
# ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd"
# # => ["David", "Rafael"]
#
# ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ]
# # => ["David", "Rafael"]
#
# {foo: 1, bar: 2, baz: 3}.excluding :bar
# # => {foo: 1, baz: 3}
- 23
def excluding(*elements)
elements.flatten!(1)
reject { |element| elements.include?(element) }
end
# Alias for #excluding.
- 23
def without(*elements)
excluding(*elements)
end
# Extract the given key from each element in the enumerable.
#
# [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
# # => ["David", "Rafael", "Aaron"]
#
# [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
# # => [[1, "David"], [2, "Rafael"]]
- 23
def pluck(*keys)
if keys.many?
map { |element| keys.map { |key| element[key] } }
else
key = keys.first
map { |element| element[key] }
end
end
# Extract the given key from the first element in the enumerable.
#
# [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pick(:name)
# # => "David"
#
# [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pick(:id, :name)
# # => [1, "David"]
- 23
def pick(*keys)
return if none?
if keys.many?
keys.map { |key| first[key] }
else
first[keys.first]
end
end
# Returns a new +Array+ without the blank items.
# Uses Object#blank? for determining if an item is blank.
#
# [1, "", nil, 2, " ", [], {}, false, true].compact_blank
# # => [1, 2, true]
#
# Set.new([nil, "", 1, 2])
# # => [2, 1] (or [1, 2])
#
# When called on a +Hash+, returns a new +Hash+ without the blank values.
#
# { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
# #=> { b: 1, f: true }
- 23
def compact_blank
reject(&:blank?)
end
end
- 23
class Hash
# Hash#reject has its own definition, so this needs one too.
- 23
def compact_blank #:nodoc:
reject { |_k, v| v.blank? }
end
# Removes all blank values from the +Hash+ in place and returns self.
# Uses Object#blank? for determining if a value is blank.
#
# h = { a: "", b: 1, c: nil, d: [], e: false, f: true }
# h.compact_blank!
# # => { b: 1, f: true }
- 23
def compact_blank!
# use delete_if rather than reject! because it always returns self even if nothing changed
delete_if { |_k, v| v.blank? }
end
end
- 23
class Range #:nodoc:
# Optimize range sum to use arithmetic progression if a block is not given and
# we have a range of numeric values.
- 23
def sum(identity = nil)
if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
super
else
actual_last = exclude_end? ? (last - 1) : last
if actual_last >= first
sum = identity || 0
sum + (actual_last - first + 1) * (actual_last + first) / 2
else
identity || 0
end
end
end
end
# Using Refinements here in order not to expose our internal method
- 23
using Module.new {
- 23
refine Array do
- 23
alias :orig_sum :sum
end
}
- 23
class Array #:nodoc:
# Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
- 23
def sum(init = nil, &block)
if init.is_a?(Numeric) || first.is_a?(Numeric)
init ||= 0
orig_sum(init, &block)
else
super
end
end
# Removes all blank elements from the +Array+ in place and returns self.
# Uses Object#blank? for determining if an item is blank.
#
# a = [1, "", nil, 2, " ", [], {}, false, true]
# a.compact_blank!
# # => [1, 2, true]
- 23
def compact_blank!
# use delete_if rather than reject! because it always returns self even if nothing changed
delete_if(&:blank?)
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/file/atomic"
# frozen_string_literal: true
- 1
require "fileutils"
- 1
class File
# Write to a file atomically. Useful for situations where you don't
# want other processes or threads to see half-written files.
#
# File.atomic_write('important.file') do |file|
# file.write('hello')
# end
#
# This method needs to create a temporary file. By default it will create it
# in the same directory as the destination file. If you don't like this
# behavior you can provide a different directory but it must be on the
# same physical filesystem as the file you're trying to write.
#
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
# file.write('hello')
# end
- 1
def self.atomic_write(file_name, temp_dir = dirname(file_name))
require "tempfile" unless defined?(Tempfile)
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
temp_file.binmode
return_val = yield temp_file
temp_file.close
old_stat = if exist?(file_name)
# Get original file permissions
stat(file_name)
else
# If not possible, probe which are the default permissions in the
# destination directory.
probe_stat_in(dirname(file_name))
end
if old_stat
# Set correct permissions on new file
begin
chown(old_stat.uid, old_stat.gid, temp_file.path)
# This operation will affect filesystem ACL's
chmod(old_stat.mode, temp_file.path)
rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
# Overwrite original file with temp file
rename(temp_file.path, file_name)
return_val
end
end
# Private utility method.
- 1
def self.probe_stat_in(dir) #:nodoc:
basename = [
".permissions_check",
Thread.current.object_id,
Process.pid,
rand(1000000)
].join(".")
file_name = join(dir, basename)
FileUtils.touch(file_name)
stat(file_name)
ensure
FileUtils.rm_f(file_name) if file_name
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/conversions"
- 1
require "active_support/core_ext/hash/deep_merge"
- 1
require "active_support/core_ext/hash/deep_transform_values"
- 1
require "active_support/core_ext/hash/except"
- 1
require "active_support/core_ext/hash/indifferent_access"
- 1
require "active_support/core_ext/hash/keys"
- 1
require "active_support/core_ext/hash/reverse_merge"
- 1
require "active_support/core_ext/hash/slice"
# frozen_string_literal: true
require "active_support/deprecation"
ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#compact and Hash#compact! natively, so requiring active_support/core_ext/hash/compact is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
# frozen_string_literal: true
- 1
require "active_support/xml_mini"
- 1
require "active_support/core_ext/object/blank"
- 1
require "active_support/core_ext/object/to_param"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "active_support/core_ext/object/try"
- 1
require "active_support/core_ext/array/wrap"
- 1
require "active_support/core_ext/hash/reverse_merge"
- 1
require "active_support/core_ext/string/inflections"
- 1
class Hash
# Returns a string containing an XML representation of its receiver:
#
# { foo: 1, bar: 2 }.to_xml
# # =>
# # <?xml version="1.0" encoding="UTF-8"?>
# # <hash>
# # <foo type="integer">1</foo>
# # <bar type="integer">2</bar>
# # </hash>
#
# To do so, the method loops over the pairs and builds nodes that depend on
# the _values_. Given a pair +key+, +value+:
#
# * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
#
# * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
# and +key+ singularized as <tt>:children</tt>.
#
# * If +value+ is a callable object it must expect one or two arguments. Depending
# on the arity, the callable is invoked with the +options+ hash as first argument
# with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
# callable can add nodes by using <tt>options[:builder]</tt>.
#
# {foo: lambda { |options, key| options[:builder].b(key) }}.to_xml
# # => "<b>foo</b>"
#
# * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
#
# class Foo
# def to_xml(options)
# options[:builder].bar 'fooing!'
# end
# end
#
# { foo: Foo.new }.to_xml(skip_instruct: true)
# # =>
# # <hash>
# # <bar>fooing!</bar>
# # </hash>
#
# * Otherwise, a node with +key+ as tag is created with a string representation of
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
# Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
# added as well according to the following mapping:
#
# XML_TYPE_NAMES = {
# "Symbol" => "symbol",
# "Integer" => "integer",
# "BigDecimal" => "decimal",
# "Float" => "float",
# "TrueClass" => "boolean",
# "FalseClass" => "boolean",
# "Date" => "date",
# "DateTime" => "dateTime",
# "Time" => "dateTime"
# }
#
# By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
#
# The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
- 1
def to_xml(options = {})
require "active_support/builder" unless defined?(Builder::XmlMarkup)
options = options.dup
options[:indent] ||= 2
options[:root] ||= "hash"
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
builder = options[:builder]
builder.instruct! unless options.delete(:skip_instruct)
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
builder.tag!(root) do
each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
yield builder if block_given?
end
end
- 1
class << self
# Returns a Hash containing a collection of pairs when the key is the node name and the value is
# its content
#
# xml = <<-XML
# <?xml version="1.0" encoding="UTF-8"?>
# <hash>
# <foo type="integer">1</foo>
# <bar type="integer">2</bar>
# </hash>
# XML
#
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
#
# +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to
# parse this XML.
#
# Custom +disallowed_types+ can also be passed in the form of an
# array.
#
# xml = <<-XML
# <?xml version="1.0" encoding="UTF-8"?>
# <hash>
# <foo type="integer">1</foo>
# <bar type="string">"David"</bar>
# </hash>
# XML
#
# hash = Hash.from_xml(xml, ['integer'])
# # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer"
#
# Note that passing custom disallowed types will override the default types,
# which are Symbol and YAML.
- 1
def from_xml(xml, disallowed_types = nil)
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
end
# Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.
- 1
def from_trusted_xml(xml)
from_xml xml, []
end
end
end
- 1
module ActiveSupport
- 1
class XMLConverter # :nodoc:
# Raised if the XML contains attributes with type="yaml" or
# type="symbol". Read Hash#from_xml for more details.
- 1
class DisallowedType < StandardError
- 1
def initialize(type)
super "Disallowed type attribute: #{type.inspect}"
end
end
- 1
DISALLOWED_TYPES = %w(symbol yaml)
- 1
def initialize(xml, disallowed_types = nil)
@xml = normalize_keys(XmlMini.parse(xml))
@disallowed_types = disallowed_types || DISALLOWED_TYPES
end
- 1
def to_h
deep_to_h(@xml)
end
- 1
private
- 1
def normalize_keys(params)
case params
when Hash
Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ]
when Array
params.map { |v| normalize_keys(v) }
else
params
end
end
- 1
def deep_to_h(value)
case value
when Hash
process_hash(value)
when Array
process_array(value)
when String
value
else
raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
- 1
def process_hash(value)
if value.include?("type") && !value["type"].is_a?(Hash) && @disallowed_types.include?(value["type"])
raise DisallowedType, value["type"]
end
if become_array?(value)
_, entries = Array.wrap(value.detect { |k, v| not v.is_a?(String) })
if entries.nil? || value["__content__"].try(:empty?)
[]
else
case entries
when Array
entries.collect { |v| deep_to_h(v) }
when Hash
[deep_to_h(entries)]
else
raise "can't typecast #{entries.inspect}"
end
end
elsif become_content?(value)
process_content(value)
elsif become_empty_string?(value)
""
elsif become_hash?(value)
xml_value = value.transform_values { |v| deep_to_h(v) }
# Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
# how multipart uploaded files from HTML appear
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
end
end
- 1
def become_content?(value)
value["type"] == "file" || (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
end
- 1
def become_array?(value)
value["type"] == "array"
end
- 1
def become_empty_string?(value)
# { "string" => true }
# No tests fail when the second term is removed.
value["type"] == "string" && value["nil"] != "true"
end
- 1
def become_hash?(value)
!nothing?(value) && !garbage?(value)
end
- 1
def nothing?(value)
# blank or nil parsed values are represented by nil
value.blank? || value["nil"] == "true"
end
- 1
def garbage?(value)
# If the type is the only element which makes it then
# this still makes the value nil, except if type is
# an XML node(where type['value'] is a Hash)
value["type"] && !value["type"].is_a?(::Hash) && value.size == 1
end
- 1
def process_content(value)
content = value["__content__"]
if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
else
content
end
end
- 1
def process_array(value)
value.map! { |i| deep_to_h(i) }
value.length > 1 ? value : value.first
end
end
end
# frozen_string_literal: true
- 24
class Hash
# Returns a new hash with +self+ and +other_hash+ merged recursively.
#
# h1 = { a: true, b: { c: [1, 2, 3] } }
# h2 = { a: false, b: { x: [3, 4, 5] } }
#
# h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
#
# Like with Hash#merge in the standard library, a block can be provided
# to merge values:
#
# h1 = { a: 100, b: 200, c: { c1: 100 } }
# h2 = { b: 250, c: { c1: 200 } }
# h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
# # => { a: 100, b: 450, c: { c1: 300 } }
- 24
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
# Same as +deep_merge+, but modifies +self+.
- 24
def deep_merge!(other_hash, &block)
merge!(other_hash) do |key, this_val, other_val|
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
this_val.deep_merge(other_val, &block)
elsif block_given?
block.call(key, this_val, other_val)
else
other_val
end
end
end
end
# frozen_string_literal: true
- 1
class Hash
# Returns a new hash with all values converted by the block operation.
# This includes the values from the root hash and from all
# nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_values{ |value| value.to_s.upcase }
# # => {person: {name: "ROB", age: "28"}}
- 1
def deep_transform_values(&block)
_deep_transform_values_in_object(self, &block)
end
# Destructively converts all values by using the block operation.
# This includes the values from the root hash and from all
# nested hashes and arrays.
- 1
def deep_transform_values!(&block)
_deep_transform_values_in_object!(self, &block)
end
- 1
private
# Support methods for deep transforming nested hashes and arrays.
- 1
def _deep_transform_values_in_object(object, &block)
case object
when Hash
object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
when Array
object.map { |e| _deep_transform_values_in_object(e, &block) }
else
yield(object)
end
end
- 1
def _deep_transform_values_in_object!(object, &block)
case object
when Hash
object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
when Array
object.map! { |e| _deep_transform_values_in_object!(e, &block) }
else
yield(object)
end
end
end
# frozen_string_literal: true
- 24
class Hash
# Returns a hash that includes everything except given keys.
# hash = { a: true, b: false, c: nil }
# hash.except(:c) # => { a: true, b: false }
# hash.except(:a, :b) # => { c: nil }
# hash # => { a: true, b: false, c: nil }
#
# This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
def except(*keys)
slice(*self.keys - keys)
- 24
end unless method_defined?(:except)
# Removes the given keys from hash and returns it.
# hash = { a: true, b: false, c: nil }
# hash.except!(:c) # => { a: true, b: false }
# hash # => { a: true, b: false }
- 24
def except!(*keys)
keys.each { |key| delete(key) }
self
end
end
# frozen_string_literal: true
- 1
require "active_support/hash_with_indifferent_access"
- 1
class Hash
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
#
# { a: 1 }.with_indifferent_access['a'] # => 1
- 1
def with_indifferent_access
ActiveSupport::HashWithIndifferentAccess.new(self)
end
# Called when object is nested under an object that receives
# #with_indifferent_access. This method will be called on the current object
# by the enclosing object and is aliased to #with_indifferent_access by
# default. Subclasses of Hash may overwrite this method to return +self+ if
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
# desirable.
#
# b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
# # => {"b"=>1}
- 1
alias nested_under_indifferent_access with_indifferent_access
end
# frozen_string_literal: true
- 23
class Hash
# Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
# # => {"name"=>"Rob", "age"=>"28"}
- 23
def stringify_keys
transform_keys(&:to_s)
end
# Destructively converts all keys to strings. Same as
# +stringify_keys+, but modifies +self+.
- 23
def stringify_keys!
transform_keys!(&:to_s)
end
# Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+.
#
# hash = { 'name' => 'Rob', 'age' => '28' }
#
# hash.symbolize_keys
# # => {:name=>"Rob", :age=>"28"}
- 23
def symbolize_keys
transform_keys { |key| key.to_sym rescue key }
end
- 23
alias_method :to_options, :symbolize_keys
# Destructively converts all keys to symbols, as long as they respond
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
- 23
def symbolize_keys!
transform_keys! { |key| key.to_sym rescue key }
end
- 23
alias_method :to_options!, :symbolize_keys!
# Validates all keys in a hash match <tt>*valid_keys</tt>, raising
# +ArgumentError+ on a mismatch.
#
# Note that keys are treated differently than HashWithIndifferentAccess,
# meaning that string and symbol keys will not match.
#
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
- 23
def assert_valid_keys(*valid_keys)
valid_keys.flatten!
each_key do |k|
unless valid_keys.include?(k)
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
end
end
end
# Returns a new hash with all keys converted by the block operation.
# This includes the keys from the root hash and from all
# nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
- 23
def deep_transform_keys(&block)
_deep_transform_keys_in_object(self, &block)
end
# Destructively converts all keys by using the block operation.
# This includes the keys from the root hash and from all
# nested hashes and arrays.
- 23
def deep_transform_keys!(&block)
_deep_transform_keys_in_object!(self, &block)
end
# Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
# nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
- 23
def deep_stringify_keys
deep_transform_keys(&:to_s)
end
# Destructively converts all keys to strings.
# This includes the keys from the root hash and from all
# nested hashes and arrays.
- 23
def deep_stringify_keys!
deep_transform_keys!(&:to_s)
end
# Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+. This includes the keys from the root hash
# and from all nested hashes and arrays.
#
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
# hash.deep_symbolize_keys
# # => {:person=>{:name=>"Rob", :age=>"28"}}
- 23
def deep_symbolize_keys
deep_transform_keys { |key| key.to_sym rescue key }
end
# Destructively converts all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
# nested hashes and arrays.
- 23
def deep_symbolize_keys!
deep_transform_keys! { |key| key.to_sym rescue key }
end
- 23
private
# Support methods for deep transforming nested hashes and arrays.
- 23
def _deep_transform_keys_in_object(object, &block)
case object
when Hash
object.each_with_object({}) do |(key, value), result|
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
end
when Array
object.map { |e| _deep_transform_keys_in_object(e, &block) }
else
object
end
end
- 23
def _deep_transform_keys_in_object!(object, &block)
case object
when Hash
object.keys.each do |key|
value = object.delete(key)
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
end
object
when Array
object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
else
object
end
end
end
# frozen_string_literal: true
- 1
class Hash
# Merges the caller into +other_hash+. For example,
#
# options = options.reverse_merge(size: 25, velocity: 10)
#
# is equivalent to
#
# options = { size: 25, velocity: 10 }.merge(options)
#
# This is particularly useful for initializing an options hash
# with default values.
- 1
def reverse_merge(other_hash)
other_hash.merge(self)
end
- 1
alias_method :with_defaults, :reverse_merge
# Destructive +reverse_merge+.
- 1
def reverse_merge!(other_hash)
replace(reverse_merge(other_hash))
end
- 1
alias_method :reverse_update, :reverse_merge!
- 1
alias_method :with_defaults!, :reverse_merge!
end
# frozen_string_literal: true
- 24
class Hash
# Replaces the hash with only the given keys.
# Returns a hash containing the removed key/value pairs.
#
# hash = { a: 1, b: 2, c: 3, d: 4 }
# hash.slice!(:a, :b) # => {:c=>3, :d=>4}
# hash # => {:a=>1, :b=>2}
- 24
def slice!(*keys)
omit = slice(*self.keys - keys)
hash = slice(*keys)
hash.default = default
hash.default_proc = default_proc if default_proc
replace(hash)
omit
end
# Removes and returns the key/value pairs matching the given keys.
#
# { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
# { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
- 24
def extract!(*keys)
keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
end
end
# frozen_string_literal: true
require "active_support/deprecation"
ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
# frozen_string_literal: true
- 1
require "active_support/core_ext/integer/multiple"
- 1
require "active_support/core_ext/integer/inflections"
- 1
require "active_support/core_ext/integer/time"
# frozen_string_literal: true
- 1
require "active_support/inflector"
- 1
class Integer
# Ordinalize turns a number into an ordinal string used to denote the
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# 1.ordinalize # => "1st"
# 2.ordinalize # => "2nd"
# 1002.ordinalize # => "1002nd"
# 1003.ordinalize # => "1003rd"
# -11.ordinalize # => "-11th"
# -1001.ordinalize # => "-1001st"
- 1
def ordinalize
ActiveSupport::Inflector.ordinalize(self)
end
# Ordinal returns the suffix used to denote the position
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# 1.ordinal # => "st"
# 2.ordinal # => "nd"
# 1002.ordinal # => "nd"
# 1003.ordinal # => "rd"
# -11.ordinal # => "th"
# -1001.ordinal # => "st"
- 1
def ordinal
ActiveSupport::Inflector.ordinal(self)
end
end
# frozen_string_literal: true
- 1
class Integer
# Check whether the integer is evenly divisible by the argument.
#
# 0.multiple_of?(0) # => true
# 6.multiple_of?(5) # => false
# 10.multiple_of?(2) # => true
- 1
def multiple_of?(number)
number == 0 ? self == 0 : self % number == 0
end
end
# frozen_string_literal: true
- 2
require "active_support/duration"
- 2
require "active_support/core_ext/numeric/time"
- 2
class Integer
# Returns a Duration instance matching the number of months provided.
#
# 2.months # => 2 months
- 2
def months
ActiveSupport::Duration.months(self)
end
- 2
alias :month :months
# Returns a Duration instance matching the number of years provided.
#
# 2.years # => 2 years
- 2
def years
ActiveSupport::Duration.years(self)
end
- 2
alias :year :years
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/kernel/concern"
- 1
require "active_support/core_ext/kernel/reporting"
- 1
require "active_support/core_ext/kernel/singleton_class"
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/concerning"
- 1
module Kernel
- 1
module_function
# A shortcut to define a toplevel concern, not within a module.
#
# See Module::Concerning for more.
- 1
def concern(topic, &module_definition)
Object.concern topic, &module_definition
end
end
# frozen_string_literal: true
- 24
module Kernel
- 24
module_function
# Sets $VERBOSE to +nil+ for the duration of the block and back to its original
# value afterwards.
#
# silence_warnings do
# value = noisy_call # no warning voiced
# end
#
# noisy_call # warning voiced
- 24
def silence_warnings
- 48
with_warnings(nil) { yield }
end
# Sets $VERBOSE to +true+ for the duration of the block and back to its
# original value afterwards.
- 24
def enable_warnings
with_warnings(true) { yield }
end
# Sets $VERBOSE for the duration of the block and back to its original
# value afterwards.
- 24
def with_warnings(flag)
- 24
old_verbose, $VERBOSE = $VERBOSE, flag
- 24
yield
ensure
- 24
$VERBOSE = old_verbose
end
# Blocks and ignores any exception passed as argument if raised within the block.
#
# suppress(ZeroDivisionError) do
# 1/0
# puts 'This code is NOT reached'
# end
#
# puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
- 24
def suppress(*exception_classes)
yield
rescue *exception_classes
end
end
# frozen_string_literal: true
- 1
module Kernel
# class_eval on an object acts like singleton_class.class_eval.
- 1
def class_eval(*args, &block)
singleton_class.class_eval(*args, &block)
end
end
# frozen_string_literal: true
- 3
class LoadError
# Returns true if the given path name (except perhaps for the ".rb"
# extension) is the missing file which caused the exception to be raised.
- 3
def is_missing?(location)
location.delete_suffix(".rb") == path.to_s.delete_suffix(".rb")
end
end
# frozen_string_literal: true
- 3
require "active_support/core_ext/string/inflections"
- 3
module ActiveSupport
- 3
module MarshalWithAutoloading # :nodoc:
- 3
def load(source, proc = nil)
- 55997
super(source, proc)
rescue ArgumentError, NameError => exc
if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
# try loading the class/module
loaded = $1.constantize
raise unless $1 == loaded.name
# if it is an IO we need to go back to read the object
source.rewind if source.respond_to?(:rewind)
retry
else
raise exc
end
end
end
end
- 3
Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading)
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/aliasing"
- 1
require "active_support/core_ext/module/introspection"
- 1
require "active_support/core_ext/module/anonymous"
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "active_support/core_ext/module/attribute_accessors_per_thread"
- 1
require "active_support/core_ext/module/attr_internal"
- 1
require "active_support/core_ext/module/concerning"
- 1
require "active_support/core_ext/module/delegation"
- 1
require "active_support/core_ext/module/deprecation"
- 1
require "active_support/core_ext/module/redefine_method"
- 1
require "active_support/core_ext/module/remove_method"
# frozen_string_literal: true
- 3
class Module
# Allows you to make aliases for attributes, which includes
# getter, setter, and a predicate.
#
# class Content < ActiveRecord::Base
# # has a title attribute
# end
#
# class Email < Content
# alias_attribute :subject, :title
# end
#
# e = Email.find(1)
# e.title # => "Superstars"
# e.subject # => "Superstars"
# e.subject? # => true
# e.subject = "Megastars"
# e.title # => "Megastars"
- 3
def alias_attribute(new_name, old_name)
# The following reader methods use an explicit `self` receiver in order to
# support aliases that start with an uppercase letter. Otherwise, they would
# be resolved as constants instead.
- 2
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
end
# frozen_string_literal: true
- 3
class Module
# A module may or may not have a name.
#
# module M; end
# M.name # => "M"
#
# m = Module.new
# m.name # => nil
#
# +anonymous?+ method returns true if module does not have a name, false otherwise:
#
# Module.new.anonymous? # => true
#
# module M; end
# M.anonymous? # => false
#
# A module gets a name when it is first assigned to a constant. Either
# via the +module+ or +class+ keyword or by an explicit assignment:
#
# m = Module.new # creates an anonymous module
# m.anonymous? # => true
# M = m # m gets a name here as a side-effect
# m.name # => "M"
# m.anonymous? # => false
- 3
def anonymous?
name.nil?
end
end
# frozen_string_literal: true
- 1
class Module
# Declares an attribute reader backed by an internally-named instance variable.
- 1
def attr_internal_reader(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
end
# Declares an attribute writer backed by an internally-named instance variable.
- 1
def attr_internal_writer(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
end
# Declares an attribute reader and writer backed by an internally-named instance
# variable.
- 1
def attr_internal_accessor(*attrs)
attr_internal_reader(*attrs)
attr_internal_writer(*attrs)
end
- 1
alias_method :attr_internal, :attr_internal_accessor
- 2
class << self; attr_accessor :attr_internal_naming_format end
- 1
self.attr_internal_naming_format = "@_%s"
- 1
private
- 1
def attr_internal_ivar_name(attr)
Module.attr_internal_naming_format % attr
end
- 1
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
# use native attr_* methods as they are faster on some Ruby implementations
send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
end
end
# frozen_string_literal: true
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes.
- 24
class Module
# Defines a class attribute and creates a class and instance reader methods.
# The underlying class variable is set to +nil+, if it is not previously
# defined. All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_reader :hair_colors
# end
#
# HairColors.hair_colors # => nil
# HairColors.class_variable_set("@@hair_colors", [:brown, :black])
# HairColors.hair_colors # => [:brown, :black]
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_reader :hair_colors, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
- 24
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
- 280
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
- 280
location ||= caller_locations(1, 1).first
- 280
definition = []
- 280
syms.each do |sym|
- 280
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
- 280
definition << "def self.#{sym}; @@#{sym}; end"
- 280
if instance_reader && instance_accessor
- 252
definition << "def #{sym}; @@#{sym}; end"
end
- 280
sym_default_value = (block_given? && default.nil?) ? yield : default
- 280
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
- 280
module_eval(definition.join(";"), location.path, location.lineno)
end
- 24
alias :cattr_reader :mattr_reader
# Defines a class attribute and creates a class and instance writer methods to
# allow assignment to the attribute. All class and instance methods created
# will be public, even if this method is called with a private or protected
# access modifier.
#
# module HairColors
# mattr_writer :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black]
# Person.class_variable_get("@@hair_colors") # => [:brown, :black]
# Person.new.hair_colors = [:blonde, :red]
# HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_writer :hair_colors, instance_writer: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- 24
def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil, location: nil)
- 186
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
- 186
location ||= caller_locations(1, 1).first
- 186
definition = []
- 186
syms.each do |sym|
- 186
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
- 186
definition << "def self.#{sym}=(val); @@#{sym} = val; end"
- 186
if instance_writer && instance_accessor
- 110
definition << "def #{sym}=(val); @@#{sym} = val; end"
end
- 186
sym_default_value = (block_given? && default.nil?) ? yield : default
- 186
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
- 186
module_eval(definition.join(";"), location.path, location.lineno)
end
- 24
alias :cattr_writer :mattr_writer
# Defines both class and instance accessors for class attributes.
# All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_accessor :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black, :blonde, :red]
# HairColors.hair_colors # => [:brown, :black, :blonde, :red]
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
#
# If a subclass changes the value then that would also change the value for
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
# class Citizen < Person
# end
#
# Citizen.new.hair_colors << :blue
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# module HairColors
# mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# module HairColors
# mattr_accessor :hair_colors, instance_accessor: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- 24
def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
- 186
location = caller_locations(1, 1).first
- 186
mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, location: location, &blk)
- 186
mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default, location: location)
end
- 24
alias :cattr_accessor :mattr_accessor
end
# frozen_string_literal: true
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes, but does so on a per-thread basis.
#
# So the values are scoped within the Thread.current space under the class name
# of the module.
- 1
class Module
# Defines a per-thread class attribute and creates class and instance reader methods.
# The underlying per-thread class variable is set to +nil+, if it is not previously defined.
#
# module Current
# thread_mattr_reader :user
# end
#
# Current.user # => nil
# Thread.current[:attr_Current_user] = "DHH"
# Current.user # => "DHH"
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# thread_mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_reader :user, instance_reader: false
# end
#
# Current.new.user # => NoMethodError
- 1
def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
- 4
syms.each do |sym|
- 4
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
- 4
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}
Thread.current["attr_" + name + "_#{sym}"]
end
EOS
- 4
if instance_reader && instance_accessor
- 2
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
self.class.#{sym}
end
EOS
end
- 4
Thread.current["attr_" + name + "_#{sym}"] = default unless default.nil?
end
end
- 1
alias :thread_cattr_reader :thread_mattr_reader
# Defines a per-thread class attribute and creates a class and instance writer methods to
# allow assignment to the attribute.
#
# module Current
# thread_mattr_writer :user
# end
#
# Current.user = "DHH"
# Thread.current[:attr_Current_user] # => "DHH"
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_writer :user, instance_writer: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
- 1
def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
- 3
syms.each do |sym|
- 3
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
- 3
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}=(obj)
Thread.current["attr_" + name + "_#{sym}"] = obj
end
EOS
- 3
if instance_writer && instance_accessor
- 1
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
self.class.#{sym} = obj
end
EOS
end
- 3
public_send("#{sym}=", default) unless default.nil?
end
end
- 1
alias :thread_cattr_writer :thread_mattr_writer
# Defines both class and instance accessors for class attributes.
#
# class Account
# thread_mattr_accessor :user
# end
#
# Account.user = "DHH"
# Account.user # => "DHH"
# Account.new.user # => "DHH"
#
# If a subclass changes the value, the parent class' value is not changed.
# Similarly, if the parent class changes the value, the value of subclasses
# is not changed.
#
# class Customer < Account
# end
#
# Customer.user = "Rafael"
# Customer.user # => "Rafael"
# Account.user # => "DHH"
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class Current
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class Current
# thread_mattr_accessor :user, instance_accessor: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
- 1
def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
- 3
thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
- 3
thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
end
- 1
alias :thread_cattr_accessor :thread_mattr_accessor
end
# frozen_string_literal: true
- 1
require "active_support/concern"
- 1
class Module
# = Bite-sized separation of concerns
#
# We often find ourselves with a medium-sized chunk of behavior that we'd
# like to extract, but only mix in to a single class.
#
# Extracting a plain old Ruby object to encapsulate it and collaborate or
# delegate to the original object is often a good choice, but when there's
# no additional state to encapsulate or we're making DSL-style declarations
# about the parent class, introducing new collaborators can obfuscate rather
# than simplify.
#
# The typical route is to just dump everything in a monolithic class, perhaps
# with a comment, as a least-bad alternative. Using modules in separate files
# means tedious sifting to get a big-picture view.
#
# = Dissatisfying ways to separate small concerns
#
# == Using comments:
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# ## Event tracking
# has_many :events
#
# before_create :track_creation
#
# private
# def track_creation
# # ...
# end
# end
#
# == With an inline module:
#
# Noisy syntax.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# module EventTracking
# extend ActiveSupport::Concern
#
# included do
# has_many :events
# before_create :track_creation
# end
#
# private
# def track_creation
# # ...
# end
# end
# include EventTracking
# end
#
# == Mix-in noise exiled to its own file:
#
# Once our chunk of behavior starts pushing the scroll-to-understand-it
# boundary, we give in and move it to a separate file. At this size, the
# increased overhead can be a reasonable tradeoff even if it reduces our
# at-a-glance perception of how things work.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# include TodoEventTracking
# end
#
# = Introducing Module#concerning
#
# By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
# separate bite-sized concerns.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# concerning :EventTracking do
# included do
# has_many :events
# before_create :track_creation
# end
#
# private
# def track_creation
# # ...
# end
# end
# end
#
# Todo.ancestors
# # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
#
# This small step has some wonderful ripple effects. We can
# * grok the behavior of our class in one glance,
# * clean up monolithic junk-drawer classes by separating their concerns, and
# * stop leaning on protected/private for crude "this is internal stuff" modularity.
#
# === Prepending `concerning`
#
# `concerning` supports a `prepend: true` argument which will `prepend` the
# concern instead of using `include` for it.
- 1
module Concerning
# Define a new concern and mix it in.
- 1
def concerning(topic, prepend: false, &block)
- 2
method = prepend ? :prepend : :include
- 2
__send__(method, concern(topic, &block))
end
# A low-cruft shortcut to define a concern.
#
# concern :EventTracking do
# ...
# end
#
# is equivalent to
#
# module EventTracking
# extend ActiveSupport::Concern
#
# ...
# end
- 1
def concern(topic, &module_definition)
- 2
const_set topic, Module.new {
- 2
extend ::ActiveSupport::Concern
- 2
module_eval(&module_definition)
}
end
end
- 1
include Concerning
end
# frozen_string_literal: true
- 24
require "set"
- 24
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
# option is not used.
- 24
class DelegationError < NoMethodError; end
- 24
RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
- 24
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
- 24
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
# public methods as your own.
#
# ==== Options
# * <tt>:to</tt> - Specifies the target object name as a symbol or string
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
# * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
# from being raised
# * <tt>:private</tt> - If set to true, changes method visibility to private
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
# (also a symbol or string).
#
# Delegation is particularly useful with Active Record associations:
#
# class Greeter < ActiveRecord::Base
# def hello
# 'hello'
# end
#
# def goodbye
# 'goodbye'
# end
# end
#
# class Foo < ActiveRecord::Base
# belongs_to :greeter
# delegate :hello, to: :greeter
# end
#
# Foo.new.hello # => "hello"
# Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
#
# Multiple delegates to the same target are allowed:
#
# class Foo < ActiveRecord::Base
# belongs_to :greeter
# delegate :hello, :goodbye, to: :greeter
# end
#
# Foo.new.goodbye # => "goodbye"
#
# Methods can be delegated to instance variables, class variables, or constants
# by providing them as a symbols:
#
# class Foo
# CONSTANT_ARRAY = [0,1,2,3]
# @@class_array = [4,5,6,7]
#
# def initialize
# @instance_array = [8,9,10,11]
# end
# delegate :sum, to: :CONSTANT_ARRAY
# delegate :min, to: :@@class_array
# delegate :max, to: :@instance_array
# end
#
# Foo.new.sum # => 6
# Foo.new.min # => 4
# Foo.new.max # => 11
#
# It's also possible to delegate a method to the class by using +:class+:
#
# class Foo
# def self.hello
# "world"
# end
#
# delegate :hello, to: :class
# end
#
# Foo.new.hello # => "world"
#
# Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
# is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
# delegated to.
#
# Person = Struct.new(:name, :address)
#
# class Invoice < Struct.new(:client)
# delegate :name, :address, to: :client, prefix: true
# end
#
# john_doe = Person.new('John Doe', 'Vimmersvej 13')
# invoice = Invoice.new(john_doe)
# invoice.client_name # => "John Doe"
# invoice.client_address # => "Vimmersvej 13"
#
# It is also possible to supply a custom prefix.
#
# class Invoice < Struct.new(:client)
# delegate :name, :address, to: :client, prefix: :customer
# end
#
# invoice = Invoice.new(john_doe)
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
# The delegated methods are public by default.
# Pass <tt>private: true</tt> to change that.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :first_name, to: :profile
# delegate :date_of_birth, to: :profile, private: true
#
# def age
# Date.today.year - date_of_birth.year
# end
# end
#
# User.new.first_name # => "Tomas"
# User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
# User.new.age # => 2
#
# If the target is +nil+ and does not respond to the delegated method a
# +Module::DelegationError+ is raised. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile
# end
#
# User.new.age
# # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
#
# But if not having a profile yet is fine and should not be an error
# condition:
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile, allow_nil: true
# end
#
# User.new.age # nil
#
# Note that if the target is not +nil+ then the call is attempted regardless of the
# <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
# does not respond to the method:
#
# class Foo
# def initialize(bar)
# @bar = bar
# end
#
# delegate :name, to: :@bar, allow_nil: true
# end
#
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
# The target method must be public, otherwise it will raise +NoMethodError+.
- 24
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
- 558
unless to
raise ArgumentError, "Delegation needs a target. Supply a keyword argument 'to' (e.g. delegate :hello, to: :greeter)."
end
- 558
if prefix == true && /^[^a-z_]/.match?(to)
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
- 558
method_prefix = \
- 558
if prefix
- 6
"#{prefix == true ? to : prefix}_"
else
- 552
""
end
- 558
location = caller_locations(1, 1).first
- 558
file, line = location.path, location.lineno
- 558
to = to.to_s
- 558
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
- 558
method_def = []
- 558
method_names = []
- 558
methods.map do |method|
- 699
method_name = prefix ? "#{method_prefix}#{method}" : method
- 699
method_names << method_name.to_sym
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
- 699
definition = if /[^\]]=$/.match?(method)
- 170
"arg"
- 529
elsif RUBY_VERSION >= "2.7"
"..."
else
- 529
"*args, &block"
end
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
- 699
if allow_nil
- 4
method = method.to_s
method_def <<
"def #{method_name}(#{definition})" <<
" _ = #{to}" <<
" if !_.nil? || nil.respond_to?(:#{method})" <<
" _.#{method}(#{definition})" <<
- 4
" end" <<
"end"
else
- 695
method = method.to_s
- 695
method_name = method_name.to_s
method_def <<
"def #{method_name}(#{definition})" <<
" _ = #{to}" <<
" _.#{method}(#{definition})" <<
"rescue NoMethodError => e" <<
" if _.nil? && e.name == :#{method}" <<
%( raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") <<
" else" <<
" raise" <<
- 695
" end" <<
"end"
end
end
- 558
module_eval(method_def.join(";"), file, line)
- 558
private(*method_names) if private
- 558
method_names
end
# When building decorators, a common pattern may emerge:
#
# class Partition
# def initialize(event)
# @event = event
# end
#
# def person
# detail.person || creator
# end
#
# private
# def respond_to_missing?(name, include_private = false)
# @event.respond_to?(name, include_private)
# end
#
# def method_missing(method, *args, &block)
# @event.send(method, *args, &block)
# end
# end
#
# With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
#
# class Partition
# delegate_missing_to :@event
#
# def initialize(event)
# @event = event
# end
#
# def person
# detail.person || creator
# end
# end
#
# The target can be anything callable within the object, e.g. instance
# variables, methods, constants, etc.
#
# The delegated method must be public on the target, otherwise it will
# raise +DelegationError+. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
#
# The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
# delegation due to possible interference when calling
# <tt>Marshal.dump(object)</tt>, should the delegation target method
# of <tt>object</tt> add or remove instance variables.
- 24
def delegate_missing_to(target, allow_nil: nil)
- 6
target = target.to_s
- 6
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
- 6
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def respond_to_missing?(name, include_private = false)
# It may look like an oversight, but we deliberately do not pass
# +include_private+, because they do not get delegated.
return false if name == :marshal_dump || name == :_dump
#{target}.respond_to?(name) || super
end
def method_missing(method, *args, &block)
if #{target}.respond_to?(method)
#{target}.public_send(method, *args, &block)
else
begin
super
rescue NoMethodError
if #{target}.nil?
if #{allow_nil == true}
nil
else
raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
end
else
raise
end
end
end
end
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
RUBY
end
end
# frozen_string_literal: true
- 23
class Module
# deprecate :foo
# deprecate bar: 'message'
# deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
#
# You can also use custom deprecator instance:
#
# deprecate :foo, deprecator: MyLib::Deprecator.new
# deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
#
# \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
# method where you can implement your custom warning behavior.
#
# class MyLib::Deprecator
# def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
# message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
# Kernel.warn message
# end
# end
- 23
def deprecate(*method_names)
- 4
ActiveSupport::Deprecation.deprecate_methods(self, *method_names)
end
end
# frozen_string_literal: true
- 3
require "active_support/core_ext/string/filters"
- 3
require "active_support/inflector"
- 3
class Module
# Returns the name of the module containing this one.
#
# M::N.module_parent_name # => "M"
- 3
def module_parent_name
if defined?(@parent_name)
@parent_name
else
parent_name = name =~ /::[^:]+\z/ ? -$` : nil
@parent_name = parent_name unless frozen?
parent_name
end
end
- 3
def parent_name
ActiveSupport::Deprecation.warn(<<-MSG.squish)
`Module#parent_name` has been renamed to `module_parent_name`.
`parent_name` is deprecated and will be removed in Rails 6.1.
MSG
module_parent_name
end
# Returns the module which contains this one according to its name.
#
# module M
# module N
# end
# end
# X = M::N
#
# M::N.module_parent # => M
# X.module_parent # => M
#
# The parent of top-level and anonymous modules is Object.
#
# M.module_parent # => Object
# Module.new.module_parent # => Object
- 3
def module_parent
module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object
end
- 3
def parent
ActiveSupport::Deprecation.warn(<<-MSG.squish)
`Module#parent` has been renamed to `module_parent`.
`parent` is deprecated and will be removed in Rails 6.1.
MSG
module_parent
end
# Returns all the parents of this module according to its name, ordered from
# nested outwards. The receiver is not contained within the result.
#
# module M
# module N
# end
# end
# X = M::N
#
# M.module_parents # => [Object]
# M::N.module_parents # => [M, Object]
# X.module_parents # => [M, Object]
- 3
def module_parents
parents = []
if module_parent_name
parts = module_parent_name.split("::")
until parts.empty?
parents << ActiveSupport::Inflector.constantize(parts * "::")
parts.pop
end
end
parents << Object unless parents.include? Object
parents
end
- 3
def parents
ActiveSupport::Deprecation.warn(<<-MSG.squish)
`Module#parents` has been renamed to `module_parents`.
`parents` is deprecated and will be removed in Rails 6.1.
MSG
module_parents
end
end
# frozen_string_literal: true
require "active_support/core_ext/module/anonymous"
require "active_support/core_ext/string/inflections"
ActiveSupport::Deprecation.warn("reachable is deprecated and will be removed from the framework.")
# frozen_string_literal: true
- 23
class Module
# Marks the named method as intended to be redefined, if it exists.
# Suppresses the Ruby method redefinition warning. Prefer
# #redefine_method where possible.
- 23
def silence_redefinition_of_method(method)
- 966
if method_defined?(method) || private_method_defined?(method)
# This suppresses the "method redefined" warning; the self-alias
# looks odd, but means we don't need to generate a unique name
- 916
alias_method method, method
end
end
# Replaces the existing method definition, if there is one, with the passed
# block as its body.
- 23
def redefine_method(method, &block)
- 340
visibility = method_visibility(method)
- 340
silence_redefinition_of_method(method)
- 340
define_method(method, &block)
- 340
send(visibility, method)
end
# Replaces the existing singleton method definition, if there is one, with
# the passed block as its body.
- 23
def redefine_singleton_method(method, &block)
- 332
singleton_class.redefine_method(method, &block)
end
- 23
def method_visibility(method) # :nodoc:
case
when private_method_defined?(method)
:private
when protected_method_defined?(method)
:protected
else
- 340
:public
- 340
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/redefine_method"
- 1
class Module
# Removes the named method, if it exists.
- 1
def remove_possible_method(method)
if method_defined?(method) || private_method_defined?(method)
undef_method(method)
end
end
# Removes the named singleton method, if it exists.
- 1
def remove_possible_singleton_method(method)
singleton_class.remove_possible_method(method)
end
end
# frozen_string_literal: true
- 3
class NameError
# Extract the name of the missing constant from the exception message.
#
# begin
# HelloWorld
# rescue NameError => e
# e.missing_name
# end
# # => "HelloWorld"
- 3
def missing_name
# Since ruby v2.3.0 `did_you_mean` gem is loaded by default.
# It extends NameError#message with spell corrections which are SLOW.
# We should use original_message message instead.
message = respond_to?(:original_message) ? original_message : self.message
return unless message.start_with?("uninitialized constant ")
receiver = begin
self.receiver
rescue ArgumentError
nil
end
if receiver == Object
name.to_s
elsif receiver
"#{real_mod_name(receiver)}::#{self.name}"
else
if match = message.match(/((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/)
match[1]
end
end
end
# Was this exception raised because the given name was missing?
#
# begin
# HelloWorld
# rescue NameError => e
# e.missing_name?("HelloWorld")
# end
# # => true
- 3
def missing_name?(name)
if name.is_a? Symbol
self.name == name
else
missing_name == name.to_s
end
end
- 3
private
- 3
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
- 3
private_constant :UNBOUND_METHOD_MODULE_NAME
- 3
if UnboundMethod.method_defined?(:bind_call)
def real_mod_name(mod)
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
end
else
- 3
def real_mod_name(mod)
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/numeric/bytes"
- 1
require "active_support/core_ext/numeric/time"
- 1
require "active_support/core_ext/numeric/conversions"
# frozen_string_literal: true
- 13
class Numeric
- 13
KILOBYTE = 1024
- 13
MEGABYTE = KILOBYTE * 1024
- 13
GIGABYTE = MEGABYTE * 1024
- 13
TERABYTE = GIGABYTE * 1024
- 13
PETABYTE = TERABYTE * 1024
- 13
EXABYTE = PETABYTE * 1024
# Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes
#
# 2.bytes # => 2
- 13
def bytes
self
end
- 13
alias :byte :bytes
# Returns the number of bytes equivalent to the kilobytes provided.
#
# 2.kilobytes # => 2048
- 13
def kilobytes
- 20
self * KILOBYTE
end
- 13
alias :kilobyte :kilobytes
# Returns the number of bytes equivalent to the megabytes provided.
#
# 2.megabytes # => 2_097_152
- 13
def megabytes
self * MEGABYTE
end
- 13
alias :megabyte :megabytes
# Returns the number of bytes equivalent to the gigabytes provided.
#
# 2.gigabytes # => 2_147_483_648
- 13
def gigabytes
self * GIGABYTE
end
- 13
alias :gigabyte :gigabytes
# Returns the number of bytes equivalent to the terabytes provided.
#
# 2.terabytes # => 2_199_023_255_552
- 13
def terabytes
self * TERABYTE
end
- 13
alias :terabyte :terabytes
# Returns the number of bytes equivalent to the petabytes provided.
#
# 2.petabytes # => 2_251_799_813_685_248
- 13
def petabytes
self * PETABYTE
end
- 13
alias :petabyte :petabytes
# Returns the number of bytes equivalent to the exabytes provided.
#
# 2.exabytes # => 2_305_843_009_213_693_952
- 13
def exabytes
self * EXABYTE
end
- 13
alias :exabyte :exabytes
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/big_decimal/conversions"
- 1
require "active_support/number_helper"
- 1
require "active_support/core_ext/module/deprecation"
- 1
module ActiveSupport
- 1
module NumericWithFormat
# Provides options for converting numbers into formatted strings.
# Options are provided for phone numbers, currency, percentage,
# precision, positional notation, file size and pretty printing.
#
# ==== Options
#
# For details on which formats use which options, see ActiveSupport::NumberHelper
#
# ==== Examples
#
# Phone Numbers:
# 5551234.to_s(:phone) # => "555-1234"
# 1235551234.to_s(:phone) # => "123-555-1234"
# 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
# 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
# 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
# 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
# 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
# # => "+1.123.555.1234 x 1343"
#
# Currency:
# 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
# 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
# 1234567890.506.to_s(:currency, round_mode: :down) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
# # => "($1,234,567,890.50)"
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '')
# # => "£1234567890,50"
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u')
# # => "1234567890,50 £"
#
# Percentage:
# 100.to_s(:percentage) # => "100.000%"
# 100.to_s(:percentage, precision: 0) # => "100%"
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
# 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
# 302.24398923423.to_s(:percentage, round_mode: :down) # => "302.243%"
# 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
# 100.to_s(:percentage, format: '%n %') # => "100.000 %"
#
# Delimited:
# 12345678.to_s(:delimited) # => "12,345,678"
# 12345678.05.to_s(:delimited) # => "12,345,678.05"
# 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
# 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
# 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
# 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
# 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
# # => "98 765 432,98"
#
# Rounded:
# 111.2345.to_s(:rounded) # => "111.235"
# 111.2345.to_s(:rounded, precision: 2) # => "111.23"
# 111.2345.to_s(:rounded, precision: 2, round_mode: :up) # => "111.24"
# 13.to_s(:rounded, precision: 5) # => "13.00000"
# 389.32314.to_s(:rounded, precision: 0) # => "389"
# 111.2345.to_s(:rounded, significant: true) # => "111"
# 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
# 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
# 111.234.to_s(:rounded, locale: :fr) # => "111,234"
# 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
# # => "13"
# 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
# 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
# # => "1.111,23"
#
# Human-friendly size in Bytes:
# 123.to_s(:human_size) # => "123 Bytes"
# 1234.to_s(:human_size) # => "1.21 KB"
# 12345.to_s(:human_size) # => "12.1 KB"
# 1234567.to_s(:human_size) # => "1.18 MB"
# 1234567890.to_s(:human_size) # => "1.15 GB"
# 1234567890123.to_s(:human_size) # => "1.12 TB"
# 1234567890123456.to_s(:human_size) # => "1.1 PB"
# 1234567890123456789.to_s(:human_size) # => "1.07 EB"
# 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
# 1234567.to_s(:human_size, precision: 2, round_mode: :up) # => "1.3 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.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
#
# Human-friendly format:
# 123.to_s(:human) # => "123"
# 1234.to_s(:human) # => "1.23 Thousand"
# 12345.to_s(:human) # => "12.3 Thousand"
# 1234567.to_s(:human) # => "1.23 Million"
# 1234567890.to_s(:human) # => "1.23 Billion"
# 1234567890123.to_s(:human) # => "1.23 Trillion"
# 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
# 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
# 489939.to_s(:human, precision: 2) # => "490 Thousand"
# 489939.to_s(:human, precision: 2, round_mode: :down) # => "480 Thousand"
# 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
# 1234567.to_s(:human, precision: 4,
# significant: false) # => "1.2346 Million"
# 1234567.to_s(:human, precision: 1,
# separator: ',',
# significant: false) # => "1,2 Million"
- 1
def to_s(format = nil, options = nil)
- 4699
case format
when nil
- 4699
super()
when Integer, String
super(format)
when :phone
ActiveSupport::NumberHelper.number_to_phone(self, options || {})
when :currency
ActiveSupport::NumberHelper.number_to_currency(self, options || {})
when :percentage
ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
when :delimited
ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
when :rounded
ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
when :human
ActiveSupport::NumberHelper.number_to_human(self, options || {})
when :human_size
ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
when Symbol
super()
else
super(format)
end
end
end
end
- 1
Integer.prepend ActiveSupport::NumericWithFormat
- 1
Float.prepend ActiveSupport::NumericWithFormat
- 1
BigDecimal.prepend ActiveSupport::NumericWithFormat
# frozen_string_literal: true
require "active_support/deprecation"
ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Numeric#positive? and Numeric#negative? natively, so requiring active_support/core_ext/numeric/inquiry is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
# frozen_string_literal: true
- 14
require "active_support/duration"
- 14
require "active_support/core_ext/time/calculations"
- 14
require "active_support/core_ext/time/acts_like"
- 14
require "active_support/core_ext/date/calculations"
- 14
require "active_support/core_ext/date/acts_like"
- 14
class Numeric
# Returns a Duration instance matching the number of seconds provided.
#
# 2.seconds # => 2 seconds
- 14
def seconds
ActiveSupport::Duration.seconds(self)
end
- 14
alias :second :seconds
# Returns a Duration instance matching the number of minutes provided.
#
# 2.minutes # => 2 minutes
- 14
def minutes
ActiveSupport::Duration.minutes(self)
end
- 14
alias :minute :minutes
# Returns a Duration instance matching the number of hours provided.
#
# 2.hours # => 2 hours
- 14
def hours
ActiveSupport::Duration.hours(self)
end
- 14
alias :hour :hours
# Returns a Duration instance matching the number of days provided.
#
# 2.days # => 2 days
- 14
def days
ActiveSupport::Duration.days(self)
end
- 14
alias :day :days
# Returns a Duration instance matching the number of weeks provided.
#
# 2.weeks # => 2 weeks
- 14
def weeks
ActiveSupport::Duration.weeks(self)
end
- 14
alias :week :weeks
# Returns a Duration instance matching the number of fortnights provided.
#
# 2.fortnights # => 4 weeks
- 14
def fortnights
ActiveSupport::Duration.weeks(self * 2)
end
- 14
alias :fortnight :fortnights
# Returns the number of milliseconds equivalent to the seconds provided.
# Used with the standard time durations.
#
# 2.in_milliseconds # => 2000
# 1.hour.in_milliseconds # => 3600000
- 14
def in_milliseconds
self * 1000
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/acts_like"
- 1
require "active_support/core_ext/object/blank"
- 1
require "active_support/core_ext/object/duplicable"
- 1
require "active_support/core_ext/object/deep_dup"
- 1
require "active_support/core_ext/object/try"
- 1
require "active_support/core_ext/object/inclusion"
- 1
require "active_support/core_ext/object/conversions"
- 1
require "active_support/core_ext/object/instance_variables"
- 1
require "active_support/core_ext/object/json"
- 1
require "active_support/core_ext/object/to_param"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "active_support/core_ext/object/with_options"
# frozen_string_literal: true
- 23
class Object
# A duck-type assistant method. For example, Active Support extends Date
# to define an <tt>acts_like_date?</tt> method, and extends Time to define
# <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
# <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
# we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
- 23
def acts_like?(duck)
case duck
when :time
respond_to? :acts_like_time?
when :date
respond_to? :acts_like_date?
when :string
respond_to? :acts_like_string?
else
respond_to? :"acts_like_#{duck}?"
end
end
end
# frozen_string_literal: true
- 24
require "concurrent/map"
- 24
class Object
# An object is blank if it's false, empty, or a whitespace string.
# For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
#
# This simplifies
#
# !address || address.empty?
#
# to
#
# address.blank?
#
# @return [true, false]
- 24
def blank?
- 34
respond_to?(:empty?) ? !!empty? : !self
end
# An object is present if it's not blank.
#
# @return [true, false]
- 24
def present?
!blank?
end
# Returns the receiver if it's present otherwise returns +nil+.
# <tt>object.presence</tt> is equivalent to
#
# object.present? ? object : nil
#
# For example, something like
#
# state = params[:state] if params[:state].present?
# country = params[:country] if params[:country].present?
# region = state || country || 'US'
#
# becomes
#
# region = params[:state].presence || params[:country].presence || 'US'
#
# @return [Object]
- 24
def presence
self if present?
end
end
- 24
class NilClass
# +nil+ is blank:
#
# nil.blank? # => true
#
# @return [true]
- 24
def blank?
- 248
true
end
end
- 24
class FalseClass
# +false+ is blank:
#
# false.blank? # => true
#
# @return [true]
- 24
def blank?
true
end
end
- 24
class TrueClass
# +true+ is not blank:
#
# true.blank? # => false
#
# @return [false]
- 24
def blank?
false
end
end
- 24
class Array
# An array is blank if it's empty:
#
# [].blank? # => true
# [1,2,3].blank? # => false
#
# @return [true, false]
- 24
alias_method :blank?, :empty?
end
- 24
class Hash
# A hash is blank if it's empty:
#
# {}.blank? # => true
# { key: 'value' }.blank? # => false
#
# @return [true, false]
- 24
alias_method :blank?, :empty?
end
- 24
class String
- 24
BLANK_RE = /\A[[:space:]]*\z/
- 24
ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
end
# A string is blank if it's empty or contains whitespaces only:
#
# ''.blank? # => true
# ' '.blank? # => true
# "\t\n\r".blank? # => true
# ' blah '.blank? # => false
#
# Unicode whitespace is supported:
#
# "\u00a0".blank? # => true
#
# @return [true, false]
- 24
def blank?
# The regexp that matches blank strings is expensive. For the case of empty
# strings we can speed up this method (~3.5x) with an empty? call. The
# penalty for the rest of strings is marginal.
empty? ||
begin
BLANK_RE.match?(self)
rescue Encoding::CompatibilityError
ENCODED_BLANKS[self.encoding].match?(self)
end
end
end
- 24
class Numeric #:nodoc:
# No number is blank:
#
# 1.blank? # => false
# 0.blank? # => false
#
# @return [false]
- 24
def blank?
false
end
end
- 24
class Time #:nodoc:
# No Time is blank:
#
# Time.now.blank? # => false
#
# @return [false]
- 24
def blank?
false
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/to_param"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "active_support/core_ext/array/conversions"
- 1
require "active_support/core_ext/hash/conversions"
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/duplicable"
- 1
class Object
# Returns a deep copy of object if it's duplicable. If it's
# not duplicable, returns +self+.
#
# object = Object.new
# dup = object.deep_dup
# dup.instance_variable_set(:@a, 1)
#
# object.instance_variable_defined?(:@a) # => false
# dup.instance_variable_defined?(:@a) # => true
- 1
def deep_dup
duplicable? ? dup : self
end
end
- 1
class Array
# Returns a deep copy of array.
#
# array = [1, [2, 3]]
# dup = array.deep_dup
# dup[1][2] = 4
#
# array[1][2] # => nil
# dup[1][2] # => 4
- 1
def deep_dup
map(&:deep_dup)
end
end
- 1
class Hash
# Returns a deep copy of hash.
#
# hash = { a: { b: 'b' } }
# dup = hash.deep_dup
# dup[:a][:c] = 'c'
#
# hash[:a][:c] # => nil
# dup[:a][:c] # => "c"
- 1
def deep_dup
hash = dup
each_pair do |key, value|
if key.frozen? && ::String === key
hash[key] = value.deep_dup
else
hash.delete(key)
hash[key.deep_dup] = value.deep_dup
end
end
hash
end
end
# frozen_string_literal: true
#--
# Most objects are cloneable, but not all. For example you can't dup methods:
#
# method(:puts).dup # => TypeError: allocator undefined for Method
#
# Classes may signal their instances are not duplicable removing +dup+/+clone+
# or raising exceptions from them. So, to dup an arbitrary object you normally
# use an optimistic approach and are ready to catch an exception, say:
#
# arbitrary_object.dup rescue object
#
# Rails dups objects in a few critical spots where they are not that arbitrary.
# That rescue is very expensive (like 40 times slower than a predicate), and it
# is often triggered.
#
# That's why we hardcode the following cases and check duplicable? instead of
# using that rescue idiom.
#++
- 1
class Object
# Can you safely dup this object?
#
# False for method objects;
# true otherwise.
- 1
def duplicable?
true
end
end
- 1
class Method
# Methods are not duplicable:
#
# method(:puts).duplicable? # => false
# method(:puts).dup # => TypeError: allocator undefined for Method
- 1
def duplicable?
false
end
end
- 1
class UnboundMethod
# Unbound methods are not duplicable:
#
# method(:puts).unbind.duplicable? # => false
# method(:puts).unbind.dup # => TypeError: allocator undefined for UnboundMethod
- 1
def duplicable?
false
end
end
# frozen_string_literal: true
- 2
class Object
# Returns true if this object is included in the argument. Argument must be
# any object which responds to +#include?+. Usage:
#
# characters = ["Konata", "Kagami", "Tsukasa"]
# "Konata".in?(characters) # => true
#
# This will throw an +ArgumentError+ if the argument doesn't respond
# to +#include?+.
- 2
def in?(another_object)
another_object.include?(self)
rescue NoMethodError
raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
end
# Returns the receiver if it's included in the argument otherwise returns +nil+.
# Argument must be any object which responds to +#include?+. Usage:
#
# params[:bucket_type].presence_in %w( project calendar )
#
# This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+.
#
# @return [Object]
- 2
def presence_in(another_object)
in?(another_object) ? self : nil
end
end
# frozen_string_literal: true
- 2
class Object
# Returns a hash with string keys that maps instance variable names without "@" to their
# corresponding values.
#
# class C
# def initialize(x, y)
# @x, @y = x, y
# end
# end
#
# C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
- 2
def instance_values
Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }]
end
# Returns an array of instance variable names as strings including "@".
#
# class C
# def initialize(x, y)
# @x, @y = x, y
# end
# end
#
# C.new(0, 1).instance_variable_names # => ["@y", "@x"]
- 2
def instance_variable_names
instance_variables.map(&:to_s)
end
end
# frozen_string_literal: true
# Hack to load json gem first so we can overwrite its to_json.
- 2
require "json"
- 2
require "bigdecimal"
- 2
require "uri/generic"
- 2
require "pathname"
- 2
require "active_support/core_ext/big_decimal/conversions" # for #to_s
- 2
require "active_support/core_ext/hash/except"
- 2
require "active_support/core_ext/hash/slice"
- 2
require "active_support/core_ext/object/instance_variables"
- 2
require "time"
- 2
require "active_support/core_ext/time/conversions"
- 2
require "active_support/core_ext/date_time/conversions"
- 2
require "active_support/core_ext/date/conversions"
#--
# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
# their default behavior. That said, we need to define the basic to_json method in all of them,
# otherwise they will always use to_json gem implementation, which is backwards incompatible in
# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
#
# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
# calls to the original to_json method.
#
# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
# should give exactly the same results with or without active support.
- 2
module ActiveSupport
- 2
module ToJsonWithActiveSupportEncoder # :nodoc:
- 2
def to_json(options = nil)
if options.is_a?(::JSON::State)
# Called from JSON.{generate,dump}, forward it to JSON gem's to_json
super(options)
else
# to_json is being invoked directly, use ActiveSupport's encoder
ActiveSupport::JSON.encode(self, options)
end
end
end
end
- 2
[Enumerable, Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].reverse_each do |klass|
- 20
klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
end
- 2
class Object
- 2
def as_json(options = nil) #:nodoc:
if respond_to?(:to_hash)
to_hash.as_json(options)
else
instance_values.as_json(options)
end
end
end
- 2
class Struct #:nodoc:
- 2
def as_json(options = nil)
Hash[members.zip(values)].as_json(options)
end
end
- 2
class TrueClass
- 2
def as_json(options = nil) #:nodoc:
self
end
end
- 2
class FalseClass
- 2
def as_json(options = nil) #:nodoc:
self
end
end
- 2
class NilClass
- 2
def as_json(options = nil) #:nodoc:
self
end
end
- 2
class String
- 2
def as_json(options = nil) #:nodoc:
self
end
end
- 2
class Symbol
- 2
def as_json(options = nil) #:nodoc:
to_s
end
end
- 2
class Numeric
- 2
def as_json(options = nil) #:nodoc:
self
end
end
- 2
class Float
# Encoding Infinity or NaN to JSON should return "null". The default returns
# "Infinity" or "NaN" which are not valid JSON.
- 2
def as_json(options = nil) #:nodoc:
finite? ? self : nil
end
end
- 2
class BigDecimal
# A BigDecimal would be naturally represented as a JSON number. Most libraries,
# however, parse non-integer JSON numbers directly as floats. Clients using
# those libraries would get in general a wrong number and no way to recover
# other than manually inspecting the string with the JSON code itself.
#
# That's why a JSON string is returned. The JSON literal is not numeric, but
# if the other end knows by contract that the data is supposed to be a
# BigDecimal, it still has the chance to post-process the string and get the
# real value.
- 2
def as_json(options = nil) #:nodoc:
finite? ? to_s : nil
end
end
- 2
class Regexp
- 2
def as_json(options = nil) #:nodoc:
to_s
end
end
- 2
module Enumerable
- 2
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
end
- 2
class IO
- 2
def as_json(options = nil) #:nodoc:
to_s
end
end
- 2
class Range
- 2
def as_json(options = nil) #:nodoc:
to_s
end
end
- 2
class Array
- 2
def as_json(options = nil) #:nodoc:
map { |v| options ? v.as_json(options.dup) : v.as_json }
end
end
- 2
class Hash
- 2
def as_json(options = nil) #:nodoc:
# create a subset of the hash by applying :only or :except
subset = if options
if attrs = options[:only]
slice(*Array(attrs))
elsif attrs = options[:except]
except(*Array(attrs))
else
self
end
else
self
end
result = {}
subset.each do |k, v|
result[k.to_s] = options ? v.as_json(options.dup) : v.as_json
end
result
end
end
- 2
class Time
- 2
def as_json(options = nil) #:nodoc:
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
end
end
- 2
class Date
- 2
def as_json(options = nil) #:nodoc:
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
strftime("%Y-%m-%d")
else
strftime("%Y/%m/%d")
end
end
end
- 2
class DateTime
- 2
def as_json(options = nil) #:nodoc:
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
strftime("%Y/%m/%d %H:%M:%S %z")
end
end
end
- 2
class URI::Generic #:nodoc:
- 2
def as_json(options = nil)
to_s
end
end
- 2
class Pathname #:nodoc:
- 2
def as_json(options = nil)
to_s
end
end
- 2
class Process::Status #:nodoc:
- 2
def as_json(options = nil)
{ exitstatus: exitstatus, pid: pid }
end
end
- 2
class Exception
- 2
def as_json(options = nil)
to_s
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/object/to_query"
# frozen_string_literal: true
- 23
require "cgi"
- 23
class Object
# Alias of <tt>to_s</tt>.
- 23
def to_param
to_s
end
# Converts an object into a string suitable for use as a URL query string,
# using the given <tt>key</tt> as the param name.
- 23
def to_query(key)
"#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
end
end
- 23
class NilClass
# Returns +self+.
- 23
def to_param
self
end
end
- 23
class TrueClass
# Returns +self+.
- 23
def to_param
self
end
end
- 23
class FalseClass
# Returns +self+.
- 23
def to_param
self
end
end
- 23
class Array
# Calls <tt>to_param</tt> on all its elements and joins the result with
# slashes. This is used by <tt>url_for</tt> in Action Pack.
- 23
def to_param
collect(&:to_param).join "/"
end
# Converts an array into a string suitable for use as a URL query string,
# using the given +key+ as the param name.
#
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
- 23
def to_query(key)
prefix = "#{key}[]"
if empty?
nil.to_query(prefix)
else
collect { |value| value.to_query(prefix) }.join "&"
end
end
end
- 23
class Hash
# Returns a string representation of the receiver suitable for use as a URL
# query string:
#
# {name: 'David', nationality: 'Danish'}.to_query
# # => "name=David&nationality=Danish"
#
# An optional namespace can be passed to enclose key names:
#
# {name: 'David', nationality: 'Danish'}.to_query('user')
# # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
#
# The string pairs "key=value" that conform the query string
# are sorted lexicographically in ascending order.
#
# This method is also aliased as +to_param+.
- 23
def to_query(namespace = nil)
query = collect do |key, value|
unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
end
end.compact
query.sort! unless namespace.to_s.include?("[]")
query.join("&")
end
- 23
alias_method :to_param, :to_query
end
# frozen_string_literal: true
- 24
require "delegate"
- 24
module ActiveSupport
- 24
module Tryable #:nodoc:
- 24
def try(method_name = nil, *args, &b)
if method_name.nil? && block_given?
if b.arity == 0
instance_eval(&b)
else
yield self
end
elsif respond_to?(method_name)
public_send(method_name, *args, &b)
end
end
- 24
ruby2_keywords(:try) if respond_to?(:ruby2_keywords, true)
- 24
def try!(method_name = nil, *args, &b)
if method_name.nil? && block_given?
if b.arity == 0
instance_eval(&b)
else
yield self
end
else
public_send(method_name, *args, &b)
end
end
- 24
ruby2_keywords(:try!) if respond_to?(:ruby2_keywords, true)
end
end
- 24
class Object
- 24
include ActiveSupport::Tryable
##
# :method: try
#
# :call-seq:
# try(*a, &b)
#
# Invokes the public method whose name goes as first argument just like
# +public_send+ does, except that if the receiver does not respond to it the
# call returns +nil+ rather than raising an exception.
#
# This method is defined to be able to write
#
# @person.try(:name)
#
# instead of
#
# @person.name if @person
#
# +try+ calls can be chained:
#
# @person.try(:spouse).try(:name)
#
# instead of
#
# @person.spouse.name if @person && @person.spouse
#
# +try+ will also return +nil+ if the receiver does not respond to the method:
#
# @person.try(:non_existing_method) # => nil
#
# instead of
#
# @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
#
# +try+ returns +nil+ when called on +nil+ regardless of whether it responds
# to the method:
#
# nil.try(:to_i) # => nil, rather than 0
#
# Arguments and blocks are forwarded to the method if invoked:
#
# @posts.try(:each_slice, 2) do |a, b|
# ...
# end
#
# The number of arguments in the signature must match. If the object responds
# to the method the call is attempted and +ArgumentError+ is still raised
# in case of argument mismatch.
#
# If +try+ is called without arguments it yields the receiver to a given
# block unless it is +nil+:
#
# @person.try do |p|
# ...
# end
#
# You can also call try with a block without accepting an argument, and the block
# will be instance_eval'ed instead:
#
# @person.try { upcase.truncate(50) }
#
# Please also note that +try+ is defined on +Object+. Therefore, it won't work
# with instances of classes that do not have +Object+ among their ancestors,
# like direct subclasses of +BasicObject+.
##
# :method: try!
#
# :call-seq:
# try!(*a, &b)
#
# 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"
# nil.try!(:upcase) # => nil
# 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
end
- 24
class Delegator
- 24
include ActiveSupport::Tryable
##
# :method: try
#
# :call-seq:
# try(a*, &b)
#
# See Object#try
##
# :method: try!
#
# :call-seq:
# try!(a*, &b)
#
# See Object#try!
end
- 24
class NilClass
# Calling +try+ on +nil+ always returns +nil+.
# It becomes especially helpful when navigating through associations that may return +nil+.
#
# nil.try(:name) # => nil
#
# Without +try+
# @person && @person.children.any? && @person.children.first.name
#
# With +try+
# @person.try(:children).try(:first).try(:name)
- 24
def try(_method_name = nil, *)
nil
end
# Calling +try!+ on +nil+ always returns +nil+.
#
# nil.try!(:name) # => nil
- 24
def try!(_method_name = nil, *)
nil
end
end
# frozen_string_literal: true
- 2
require "active_support/option_merger"
- 2
class Object
# An elegant way to factor duplication out of options passed to a series of
# method calls. Each method called in the block, with the block variable as
# the receiver, will have its options merged with the default +options+ hash
# provided. Each method called on the block variable must take an options
# hash as its final argument.
#
# Without <tt>with_options</tt>, this code contains duplication:
#
# class Account < ActiveRecord::Base
# has_many :customers, dependent: :destroy
# has_many :products, dependent: :destroy
# has_many :invoices, dependent: :destroy
# has_many :expenses, dependent: :destroy
# end
#
# Using <tt>with_options</tt>, we can remove the duplication:
#
# class Account < ActiveRecord::Base
# with_options dependent: :destroy do |assoc|
# assoc.has_many :customers
# assoc.has_many :products
# assoc.has_many :invoices
# assoc.has_many :expenses
# end
# end
#
# It can also be used with an explicit receiver:
#
# I18n.with_options locale: user.locale, scope: 'newsletter' do |i18n|
# subject i18n.t :subject
# body i18n.t :body, user_name: user.name
# end
#
# When you don't pass an explicit receiver, it executes the whole block
# in merging options context:
#
# class Account < ActiveRecord::Base
# with_options dependent: :destroy do
# has_many :customers
# has_many :products
# has_many :invoices
# has_many :expenses
# end
# end
#
# <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
#
# NOTE: Each nesting level will merge inherited defaults in addition to their own.
#
# class Post < ActiveRecord::Base
# with_options if: :persisted?, length: { minimum: 50 } do
# validates :content, if: -> { content.present? }
# end
# end
#
# The code is equivalent to:
#
# validates :content, length: { minimum: 50 }, if: -> { content.present? }
#
# Hence the inherited default for +if+ key is ignored.
#
# NOTE: You cannot call class methods implicitly inside of with_options.
# You can access these methods using the class name instead:
#
# class Phone < ActiveRecord::Base
# enum phone_number_type: { home: 0, office: 1, mobile: 2 }
#
# with_options presence: true do
# validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys }
# end
# end
#
- 2
def with_options(options, &block)
option_merger = ActiveSupport::OptionMerger.new(self, options)
block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/range/conversions"
- 1
require "active_support/core_ext/range/compare_range"
- 1
require "active_support/core_ext/range/include_time_with_zone"
- 1
require "active_support/core_ext/range/overlaps"
- 1
require "active_support/core_ext/range/each"
# frozen_string_literal: true
- 1
module ActiveSupport
- 1
module CompareWithRange
# Extends the default Range#=== to support range comparisons.
# (1..5) === (1..5) # => true
# (1..5) === (2..3) # => true
# (1..5) === (1...6) # => true
# (1..5) === (2..6) # => false
#
# The native Range#=== behavior is untouched.
# ('a'..'f') === ('c') # => true
# (5..9) === (11) # => false
#
# The given range must be fully bounded, with both start and end.
- 1
def ===(value)
if value.is_a?(::Range)
is_backwards_op = value.exclude_end? ? :>= : :>
return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
# 1...10 includes 1..9 but it does not include 1..10.
# 1..10 includes 1...11 but it does not include 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
super(value.first) && (self.end.nil? || value_max.send(operator, last))
else
super
end
end
# Extends the default Range#include? to support range comparisons.
# (1..5).include?(1..5) # => true
# (1..5).include?(2..3) # => true
# (1..5).include?(1...6) # => true
# (1..5).include?(2..6) # => false
#
# The native Range#include? behavior is untouched.
# ('a'..'f').include?('c') # => true
# (5..9).include?(11) # => false
#
# The given range must be fully bounded, with both start and end.
- 1
def include?(value)
if value.is_a?(::Range)
is_backwards_op = value.exclude_end? ? :>= : :>
return false if value.begin && value.end && value.begin.send(is_backwards_op, value.end)
# 1...10 includes 1..9 but it does not include 1..10.
# 1..10 includes 1...11 but it does not include 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
super(value.first) && (self.end.nil? || value_max.send(operator, last))
else
super
end
end
# Extends the default Range#cover? to support range comparisons.
# (1..5).cover?(1..5) # => true
# (1..5).cover?(2..3) # => true
# (1..5).cover?(1...6) # => true
# (1..5).cover?(2..6) # => false
#
# The native Range#cover? behavior is untouched.
# ('a'..'f').cover?('c') # => true
# (5..9).cover?(11) # => false
#
# The given range must be fully bounded, with both start and end.
- 1
def cover?(value)
if value.is_a?(::Range)
is_backwards_op = value.exclude_end? ? :>= : :>
return false if value.begin && value.end && value.begin.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
super(value.first) && (self.end.nil? || value_max.send(operator, last))
else
super
end
end
end
end
- 1
Range.prepend(ActiveSupport::CompareWithRange)
# frozen_string_literal: true
- 1
module ActiveSupport
- 1
module RangeWithFormat
- 1
RANGE_FORMATS = {
db: -> (start, stop) do
case start
when String then "BETWEEN '#{start}' AND '#{stop}'"
else
"BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
end
end
}
# Convert range to a formatted string. See RANGE_FORMATS for predefined formats.
#
# range = (1..100) # => 1..100
#
# range.to_s # => "1..100"
# range.to_s(:db) # => "BETWEEN '1' AND '100'"
#
# == Adding your own range formats to to_s
# You can add your own formats to the Range::RANGE_FORMATS hash.
# Use the format name as the hash key and a Proc instance.
#
# # config/initializers/range_formats.rb
# Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" }
- 1
def to_s(format = :default)
if formatter = RANGE_FORMATS[format]
formatter.call(first, last)
else
super()
end
end
- 1
alias_method :to_default_s, :to_s
- 1
alias_method :to_formatted_s, :to_s
end
end
- 1
Range.prepend(ActiveSupport::RangeWithFormat)
# frozen_string_literal: true
- 1
require "active_support/time_with_zone"
- 1
module ActiveSupport
- 1
module EachTimeWithZone #:nodoc:
- 1
def each(&block)
- 5
ensure_iteration_allowed
- 5
super
end
- 1
def step(n = 1, &block)
ensure_iteration_allowed
super
end
- 1
private
- 1
def ensure_iteration_allowed
- 5
raise TypeError, "can't iterate from #{first.class}" if first.is_a?(TimeWithZone)
end
end
end
- 1
Range.prepend(ActiveSupport::EachTimeWithZone)
# frozen_string_literal: true
require "active_support/deprecation"
ActiveSupport::Deprecation.warn "You have required `active_support/core_ext/range/include_range`. " \
"This file will be removed in Rails 6.1. You should require `active_support/core_ext/range/compare_range` " \
"instead."
require "active_support/core_ext/range/compare_range"
# frozen_string_literal: true
- 1
require "active_support/time_with_zone"
- 1
require "active_support/deprecation"
- 1
module ActiveSupport
- 1
module IncludeTimeWithZone #:nodoc:
# Extends the default Range#include? to support ActiveSupport::TimeWithZone.
#
# (1.hour.ago..1.hour.from_now).include?(Time.current) # => true
#
- 1
def include?(value)
if self.begin.is_a?(TimeWithZone) || self.end.is_a?(TimeWithZone)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using `Range#include?` to check the inclusion of a value in
a date time range is deprecated.
It is recommended to use `Range#cover?` instead of `Range#include?` to
check the inclusion of a value in a date time range.
MSG
cover?(value)
else
super
end
end
end
end
- 1
Range.prepend(ActiveSupport::IncludeTimeWithZone)
# frozen_string_literal: true
- 1
class Range
# Compare two ranges and see if they overlap each other
# (1..5).overlaps?(4..6) # => true
# (1..5).overlaps?(7..9) # => false
- 1
def overlaps?(other)
cover?(other.first) || other.cover?(first)
end
end
# frozen_string_literal: true
- 1
class Regexp #:nodoc:
- 1
def multiline?
options & MULTILINE == MULTILINE
end
end
# frozen_string_literal: true
- 1
require "securerandom"
- 1
module SecureRandom
- 1
BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
- 1
BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a
# SecureRandom.base58 generates a random base58 string.
#
# The argument _n_ specifies the length of the random string to be generated.
#
# If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
#
# The result may contain alphanumeric characters except 0, O, I and l.
#
# p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
# p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
- 1
def self.base58(n = 16)
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
idx = byte % 64
idx = SecureRandom.random_number(58) if idx >= 58
BASE58_ALPHABET[idx]
end.join
end
# SecureRandom.base36 generates a random base36 string in lowercase.
#
# The argument _n_ specifies the length of the random string to be generated.
#
# If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
# This method can be used over +base58+ if a deterministic case key is necessary.
#
# The result will contain alphanumeric characters in lowercase.
#
# p SecureRandom.base36 # => "4kugl2pdqmscqtje"
# p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
- 1
def self.base36(n = 16)
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
idx = byte % 64
idx = SecureRandom.random_number(36) if idx >= 36
BASE36_ALPHABET[idx]
end.join
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/string/conversions"
- 1
require "active_support/core_ext/string/filters"
- 1
require "active_support/core_ext/string/multibyte"
- 1
require "active_support/core_ext/string/starts_ends_with"
- 1
require "active_support/core_ext/string/inflections"
- 1
require "active_support/core_ext/string/access"
- 1
require "active_support/core_ext/string/behavior"
- 1
require "active_support/core_ext/string/output_safety"
- 1
require "active_support/core_ext/string/exclude"
- 1
require "active_support/core_ext/string/strip"
- 1
require "active_support/core_ext/string/inquiry"
- 1
require "active_support/core_ext/string/indent"
- 1
require "active_support/core_ext/string/zones"
# frozen_string_literal: true
- 1
class String
# If you pass a single integer, returns a substring of one character at that
# position. The first character of the string is at position 0, the next at
# position 1, and so on. If a range is supplied, a substring containing
# characters at offsets given by the range is returned. In both cases, if an
# offset is negative, it is counted from the end of the string. Returns +nil+
# if the initial offset falls outside the string. Returns an empty string if
# the beginning of the range is greater than the end of the string.
#
# str = "hello"
# str.at(0) # => "h"
# str.at(1..3) # => "ell"
# str.at(-2) # => "l"
# str.at(-2..-1) # => "lo"
# str.at(5) # => nil
# str.at(5..-1) # => ""
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
# the string. In both cases, +nil+ is returned if there is no match.
#
# str = "hello"
# str.at(/lo/) # => "lo"
# str.at(/ol/) # => nil
# str.at("lo") # => "lo"
# str.at("ol") # => nil
- 1
def at(position)
self[position]
end
# Returns a substring from the given position to the end of the string.
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
# str.from(0) # => "hello"
# str.from(3) # => "lo"
# str.from(-2) # => "lo"
#
# You can mix it with +to+ method and do fun things like:
#
# str = "hello"
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
- 1
def from(position)
self[position, length]
end
# Returns a substring from the beginning of the string to the given position.
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
# str.to(0) # => "h"
# str.to(3) # => "hell"
# str.to(-2) # => "hell"
#
# You can mix it with +from+ method and do fun things like:
#
# str = "hello"
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
- 1
def to(position)
position += size if position < 0
self[0, position + 1] || +""
end
# Returns the first character. If a limit is supplied, returns a substring
# from the beginning of the string until it reaches the limit value. If the
# given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
# str.first # => "h"
# str.first(1) # => "h"
# str.first(2) # => "he"
# str.first(0) # => ""
# str.first(6) # => "hello"
- 1
def first(limit = 1)
self[0, limit] || raise(ArgumentError, "negative limit")
end
# Returns the last character of the string. If a limit is supplied, returns a substring
# from the end of the string until it reaches the limit value (counting backwards). If
# the given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
# str.last # => "o"
# str.last(1) # => "o"
# str.last(2) # => "lo"
# str.last(0) # => ""
# str.last(6) # => "hello"
- 1
def last(limit = 1)
self[[length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
end
end
# frozen_string_literal: true
- 1
class String
# Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
- 1
def acts_like_string?
true
end
end
# frozen_string_literal: true
- 2
require "date"
- 2
require "active_support/core_ext/time/calculations"
- 2
class String
# Converts a string to a Time value.
# The +form+ can be either :utc or :local (default :local).
#
# The time is parsed using Time.parse method.
# If +form+ is :local, then the time is in the system timezone.
# If the date part is missing then the current date is used and if
# the time part is missing then it is assumed to be 00:00:00.
#
# "13-12-2012".to_time # => 2012-12-13 00:00:00 +0100
# "06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100
# "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC
# "12/13/2012".to_time # => ArgumentError: argument out of range
- 2
def to_time(form = :local)
parts = Date._parse(self, false)
used_keys = %i(year mon mday hour min sec sec_fraction offset)
return if (parts.keys & used_keys).empty?
now = Time.now
time = Time.new(
parts.fetch(:year, now.year),
parts.fetch(:mon, now.month),
parts.fetch(:mday, now.day),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
parts.fetch(:offset, form == :utc ? 0 : nil)
)
form == :utc ? time.utc : time.to_time
end
# Converts a string to a Date value.
#
# "1-1-2012".to_date # => Sun, 01 Jan 2012
# "01/01/2012".to_date # => Sun, 01 Jan 2012
# "2012-12-13".to_date # => Thu, 13 Dec 2012
# "12/13/2012".to_date # => ArgumentError: invalid date
- 2
def to_date
::Date.parse(self, false) unless blank?
end
# Converts a string to a DateTime value.
#
# "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
# "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
# "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
# "12/13/2012".to_datetime # => ArgumentError: invalid date
- 2
def to_datetime
::DateTime.parse(self, false) unless blank?
end
end
# frozen_string_literal: true
- 1
class String
# The inverse of <tt>String#include?</tt>. Returns true if the string
# does not include the other string.
#
# "hello".exclude? "lo" # => false
# "hello".exclude? "ol" # => true
# "hello".exclude? ?h # => false
- 1
def exclude?(string)
!include?(string)
end
end
# frozen_string_literal: true
- 23
class String
# Returns the string, first removing all whitespace on both ends of
# the string, and then changing remaining consecutive whitespace
# groups into one space each.
#
# Note that it handles both ASCII and Unicode whitespace.
#
# %{ Multi-line
# string }.squish # => "Multi-line string"
# " foo bar \n \t boo".squish # => "foo bar boo"
- 23
def squish
dup.squish!
end
# Performs a destructive squish. See String#squish.
# str = " foo bar \n \t boo"
# str.squish! # => "foo bar boo"
# str # => "foo bar boo"
- 23
def squish!
gsub!(/[[:space:]]+/, " ")
strip!
self
end
# Returns a new string with all occurrences of the patterns removed.
# str = "foo bar test"
# str.remove(" test") # => "foo bar"
# str.remove(" test", /bar/) # => "foo "
# str # => "foo bar test"
- 23
def remove(*patterns)
dup.remove!(*patterns)
end
# Alters the string by removing all occurrences of the patterns.
# str = "foo bar test"
# str.remove!(" test", /bar/) # => "foo "
# str # => "foo "
- 23
def remove!(*patterns)
patterns.each do |pattern|
gsub! pattern, ""
end
self
end
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
#
# 'Once upon a time in a world far far away'.truncate(27)
# # => "Once upon a time in a wo..."
#
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
#
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
# # => "Once upon a time in a..."
#
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
# # => "Once upon a time in a..."
#
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
# for a total length not exceeding <tt>length</tt>:
#
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
# # => "And they f... (continued)"
- 23
def truncate(truncate_at, options = {})
return dup unless length > truncate_at
omission = options[:omission] || "..."
length_with_room_for_omission = truncate_at - omission.length
stop = \
if options[:separator]
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
else
length_with_room_for_omission
end
+"#{self[0, stop]}#{omission}"
end
# Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
# breaking string encoding by splitting multibyte characters or breaking
# grapheme clusters ("perceptual characters") by truncating at combining
# characters.
#
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
# => 20
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
# => 80
# >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
# => "🔪🔪🔪🔪…"
#
# The truncated text ends with the <tt>:omission</tt> string, defaulting
# to "…", for a total length not exceeding <tt>bytesize</tt>.
- 23
def truncate_bytes(truncate_at, omission: "…")
omission ||= ""
case
when bytesize <= truncate_at
dup
when omission.bytesize > truncate_at
raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
when omission.bytesize == truncate_at
omission.dup
else
self.class.new.tap do |cut|
cut_at = truncate_at - omission.bytesize
scan(/\X/) do |grapheme|
if cut.bytesize + grapheme.bytesize <= cut_at
cut << grapheme
else
break
end
end
cut << omission
end
end
end
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
#
# 'Once upon a time in a world far far away'.truncate_words(4)
# # => "Once upon a time..."
#
# Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
#
# 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
# # => "Once<br>upon<br>a<br>time<br>in..."
#
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
#
# 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
# # => "And they found that many... (continued)"
- 23
def truncate_words(words_count, options = {})
sep = options[:separator] || /\s+/
sep = Regexp.escape(sep.to_s) unless Regexp === sep
if self =~ /\A((?>.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
$1 + (options[:omission] || "...")
else
dup
end
end
end
# frozen_string_literal: true
- 1
class String
# Same as +indent+, except it indents the receiver in-place.
#
# Returns the indented string, or +nil+ if there was nothing to indent.
- 1
def indent!(amount, indent_string = nil, indent_empty_lines = false)
indent_string = indent_string || self[/^[ \t]/] || " "
re = indent_empty_lines ? /^/ : /^(?!$)/
gsub!(re, indent_string * amount)
end
# Indents the lines in the receiver:
#
# <<EOS.indent(2)
# def some_method
# some_code
# end
# EOS
# # =>
# def some_method
# some_code
# end
#
# The second argument, +indent_string+, specifies which indent string to
# use. The default is +nil+, which tells the method to make a guess by
# peeking at the first indented line, and fallback to a space if there is
# none.
#
# " foo".indent(2) # => " foo"
# "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
# "foo".indent(2, "\t") # => "\t\tfoo"
#
# While +indent_string+ is typically one space or tab, it may be any string.
#
# The third argument, +indent_empty_lines+, is a flag that says whether
# empty lines should be indented. Default is false.
#
# "foo\n\nbar".indent(2) # => " foo\n\n bar"
# "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
#
- 1
def indent(amount, indent_string = nil, indent_empty_lines = false)
dup.tap { |_| _.indent!(amount, indent_string, indent_empty_lines) }
end
end
# frozen_string_literal: true
- 23
require "active_support/inflector/methods"
- 23
require "active_support/inflector/transliterate"
# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
#
# 'ScaleScore'.tableize # => "scale_scores"
#
- 23
class String
# Returns the plural form of the word in the string.
#
# If the optional parameter +count+ is specified,
# the singular form will be returned if <tt>count == 1</tt>.
# For any other value of +count+ the plural will be returned.
#
# If the optional parameter +locale+ is specified,
# the word will be pluralized as a word of that language.
# By default, this parameter is set to <tt>:en</tt>.
# You must define your own inflection rules for languages other than English.
#
# 'post'.pluralize # => "posts"
# 'octopus'.pluralize # => "octopi"
# 'sheep'.pluralize # => "sheep"
# 'words'.pluralize # => "words"
# 'the blue mailman'.pluralize # => "the blue mailmen"
# 'CamelOctopus'.pluralize # => "CamelOctopi"
# 'apple'.pluralize(1) # => "apple"
# 'apple'.pluralize(2) # => "apples"
# 'ley'.pluralize(:es) # => "leyes"
# 'ley'.pluralize(1, :es) # => "ley"
#
# See ActiveSupport::Inflector.pluralize.
- 23
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
end
# The reverse of +pluralize+, returns the singular form of a word in a string.
#
# If the optional parameter +locale+ is specified,
# the word will be singularized as a word of that language.
# By default, this parameter is set to <tt>:en</tt>.
# You must define your own inflection rules for languages other than English.
#
# 'posts'.singularize # => "post"
# 'octopi'.singularize # => "octopus"
# 'sheep'.singularize # => "sheep"
# 'word'.singularize # => "word"
# 'the blue mailmen'.singularize # => "the blue mailman"
# 'CamelOctopi'.singularize # => "CamelOctopus"
# 'leyes'.singularize(:es) # => "ley"
#
# See ActiveSupport::Inflector.singularize.
- 23
def singularize(locale = :en)
ActiveSupport::Inflector.singularize(self, locale)
end
# +constantize+ tries to find a declared constant with the name specified
# in the string. It raises a NameError when the name is not in CamelCase
# or is not initialized.
#
# 'Module'.constantize # => Module
# 'Class'.constantize # => Class
# 'blargle'.constantize # => NameError: wrong constant name blargle
#
# See ActiveSupport::Inflector.constantize.
- 23
def constantize
ActiveSupport::Inflector.constantize(self)
end
# +safe_constantize+ tries to find a declared constant with the name specified
# in the string. It returns +nil+ when the name is not in CamelCase
# or is not initialized.
#
# 'Module'.safe_constantize # => Module
# 'Class'.safe_constantize # => Class
# 'blargle'.safe_constantize # => nil
#
# See ActiveSupport::Inflector.safe_constantize.
- 23
def safe_constantize
ActiveSupport::Inflector.safe_constantize(self)
end
# By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize
# is set to <tt>:lower</tt> then camelize produces lowerCamelCase.
#
# +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
#
# 'active_record'.camelize # => "ActiveRecord"
# 'active_record'.camelize(:lower) # => "activeRecord"
# 'active_record/errors'.camelize # => "ActiveRecord::Errors"
# 'active_record/errors'.camelize(:lower) # => "activeRecord::Errors"
#
# +camelize+ is also aliased as +camelcase+.
#
# See ActiveSupport::Inflector.camelize.
- 23
def camelize(first_letter = :upper)
- 3
case first_letter
when :upper
- 3
ActiveSupport::Inflector.camelize(self, true)
when :lower
ActiveSupport::Inflector.camelize(self, false)
else
raise ArgumentError, "Invalid option, use either :upper or :lower."
end
end
- 23
alias_method :camelcase, :camelize
# Capitalizes all the words and replaces some characters in the string to create
# a nicer looking title. +titleize+ is meant for creating pretty output. It is not
# used in the Rails internals.
#
# The trailing '_id','Id'.. can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# 'man from the boondocks'.titleize # => "Man From The Boondocks"
# 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
# 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id"
#
# +titleize+ is also aliased as +titlecase+.
#
# See ActiveSupport::Inflector.titleize.
- 23
def titleize(keep_id_suffix: false)
ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix)
end
- 23
alias_method :titlecase, :titleize
# The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string.
#
# +underscore+ will also change '::' to '/' to convert namespaces to paths.
#
# 'ActiveModel'.underscore # => "active_model"
# 'ActiveModel::Errors'.underscore # => "active_model/errors"
#
# See ActiveSupport::Inflector.underscore.
- 23
def underscore
- 730
ActiveSupport::Inflector.underscore(self)
end
# Replaces underscores with dashes in the string.
#
# 'puni_puni'.dasherize # => "puni-puni"
#
# See ActiveSupport::Inflector.dasherize.
- 23
def dasherize
ActiveSupport::Inflector.dasherize(self)
end
# Removes the module part from the constant expression in the string.
#
# 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
# '::Inflections'.demodulize # => "Inflections"
# ''.demodulize # => ''
#
# See ActiveSupport::Inflector.demodulize.
#
# See also +deconstantize+.
- 23
def demodulize
ActiveSupport::Inflector.demodulize(self)
end
# Removes the rightmost segment from the constant expression in the string.
#
# 'Net::HTTP'.deconstantize # => "Net"
# '::Net::HTTP'.deconstantize # => "::Net"
# 'String'.deconstantize # => ""
# '::String'.deconstantize # => ""
# ''.deconstantize # => ""
#
# See ActiveSupport::Inflector.deconstantize.
#
# See also +demodulize+.
- 23
def deconstantize
ActiveSupport::Inflector.deconstantize(self)
end
# Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
#
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
# the configured <tt>I18n.locale</tt>.
#
# class Person
# def to_param
# "#{id}-#{name.parameterize}"
# end
# end
#
# @person = Person.find(1)
# # => #<Person id: 1, name: "Donald E. Knuth">
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
#
# To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# class Person
# def to_param
# "#{id}-#{name.parameterize(preserve_case: true)}"
# end
# end
#
# @person = Person.find(1)
# # => #<Person id: 1, name: "Donald E. Knuth">
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a>
#
# See ActiveSupport::Inflector.parameterize.
- 23
def parameterize(separator: "-", preserve_case: false, locale: nil)
ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case, locale: locale)
end
# Creates the name of a table like Rails does for models to table names. This method
# uses the +pluralize+ method on the last word in the string.
#
# 'RawScaledScorer'.tableize # => "raw_scaled_scorers"
# 'ham_and_egg'.tableize # => "ham_and_eggs"
# 'fancyCategory'.tableize # => "fancy_categories"
#
# See ActiveSupport::Inflector.tableize.
- 23
def tableize
ActiveSupport::Inflector.tableize(self)
end
# Creates a class name from a plural table name like Rails does for table names to models.
# Note that this returns a string and not a class. (To convert to an actual class
# follow +classify+ with +constantize+.)
#
# 'ham_and_eggs'.classify # => "HamAndEgg"
# 'posts'.classify # => "Post"
#
# See ActiveSupport::Inflector.classify.
- 23
def classify
ActiveSupport::Inflector.classify(self)
end
# Capitalizes the first word, turns underscores into spaces, and (by default)strips a
# trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
# The capitalization of the first word can be turned off by setting the
# optional parameter +capitalize+ to false.
# By default, this parameter is true.
#
# The trailing '_id' can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# 'employee_salary'.humanize # => "Employee salary"
# 'author_id'.humanize # => "Author"
# 'author_id'.humanize(capitalize: false) # => "author"
# '_id'.humanize # => "Id"
# 'author_id'.humanize(keep_id_suffix: true) # => "Author Id"
#
# See ActiveSupport::Inflector.humanize.
- 23
def humanize(capitalize: true, keep_id_suffix: false)
ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix)
end
# Converts just the first character to uppercase.
#
# 'what a Lovely Day'.upcase_first # => "What a Lovely Day"
# 'w'.upcase_first # => "W"
# ''.upcase_first # => ""
#
# See ActiveSupport::Inflector.upcase_first.
- 23
def upcase_first
ActiveSupport::Inflector.upcase_first(self)
end
# Creates a foreign key name from a class name.
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
#
# 'Message'.foreign_key # => "message_id"
# 'Message'.foreign_key(false) # => "messageid"
# 'Admin::Post'.foreign_key # => "post_id"
#
# See ActiveSupport::Inflector.foreign_key.
- 23
def foreign_key(separate_class_name_and_id_with_underscore = true)
ActiveSupport::Inflector.foreign_key(self, separate_class_name_and_id_with_underscore)
end
end
# frozen_string_literal: true
- 1
require "active_support/string_inquirer"
- 1
require "active_support/environment_inquirer"
- 1
class String
# Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class,
# which gives you a prettier way to test for equality.
#
# env = 'production'.inquiry
# env.production? # => true
# env.development? # => false
- 1
def inquiry
ActiveSupport::StringInquirer.new(self)
end
end
# frozen_string_literal: true
- 23
require "active_support/multibyte"
- 23
class String
# == Multibyte proxy
#
# +mb_chars+ is a multibyte safe proxy for string methods.
#
# It creates and returns an instance of the ActiveSupport::Multibyte::Chars class which
# 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.
#
# >> "lj".mb_chars.upcase.to_s
# => "LJ"
#
# NOTE: Ruby 2.4 and later support native Unicode case mappings:
#
# >> "lj".upcase
# => "LJ"
#
# == Method chaining
#
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
# method chaining on the result of any of these methods.
#
# name.mb_chars.reverse.length # => 12
#
# == Interoperability and configuration
#
# The Chars object tries to be as interchangeable with String objects as possible: sorting and comparing between
# String and Char work like expected. The bang! methods change the internal string representation in the Chars
# object. Interoperability problems can be resolved easily with a +to_s+ call.
#
# For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For
# information about how to change the default Multibyte behavior see ActiveSupport::Multibyte.
- 23
def mb_chars
ActiveSupport::Multibyte.proxy_class.new(self)
end
# Returns +true+ if string has utf_8 encoding.
#
# utf_8_str = "some string".encode "UTF-8"
# iso_str = "some string".encode "ISO-8859-1"
#
# utf_8_str.is_utf8? # => true
# iso_str.is_utf8? # => false
- 23
def is_utf8?
case encoding
when Encoding::UTF_8, Encoding::US_ASCII
valid_encoding?
when Encoding::ASCII_8BIT
dup.force_encoding(Encoding::UTF_8).valid_encoding?
else
false
end
end
end
# frozen_string_literal: true
- 1
require "erb"
- 1
require "active_support/core_ext/module/redefine_method"
- 1
require "active_support/multibyte/unicode"
- 1
class ERB
- 1
module Util
- 1
HTML_ESCAPE = { "&" => "&", ">" => ">", "<" => "<", '"' => """, "'" => "'" }
- 1
JSON_ESCAPE = { "&" => '\u0026', ">" => '\u003e', "<" => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
- 1
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
- 1
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
#
# puts html_escape('is a > 0 & a < 10?')
# # => is a > 0 & a < 10?
- 1
def html_escape(s)
unwrapped_html_escape(s).html_safe
end
- 1
silence_redefinition_of_method :h
- 1
alias h html_escape
- 1
module_function :h
- 1
singleton_class.silence_redefinition_of_method :html_escape
- 1
module_function :html_escape
# HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
# This method is not for public consumption! Seriously!
- 1
def unwrapped_html_escape(s) # :nodoc:
s = s.to_s
if s.html_safe?
s
else
CGI.escapeHTML(ActiveSupport::Multibyte::Unicode.tidy_bytes(s))
end
end
- 1
module_function :unwrapped_html_escape
# A utility method for escaping HTML without affecting existing escaped entities.
#
# html_escape_once('1 < 2 & 3')
# # => "1 < 2 & 3"
#
# html_escape_once('<< Accept & Checkout')
# # => "<< Accept & Checkout"
- 1
def html_escape_once(s)
result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
s.html_safe? ? result.html_safe : result
end
- 1
module_function :html_escape_once
# A utility method for escaping HTML entities in JSON strings. Specifically, the
# &, > and < characters are replaced with their equivalent unicode escaped form -
# \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
# escaped as they are treated as newline characters in some JavaScript engines.
# These sequences have identical meaning as the original characters inside the
# context of a JSON string, so assuming the input is a valid and well-formed
# JSON value, the output will have equivalent meaning when parsed:
#
# json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
# # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
#
# json_escape(json)
# # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
#
# JSON.parse(json) == JSON.parse(json_escape(json))
# # => true
#
# The intended use case for this method is to escape JSON strings before including
# them inside a script tag to avoid XSS vulnerability:
#
# <script>
# var currentUser = <%= raw json_escape(current_user.to_json) %>;
# </script>
#
# It is necessary to +raw+ the result of +json_escape+, so that quotation marks
# don't get converted to <tt>"</tt> entities. +json_escape+ doesn't
# automatically flag the result as HTML safe, since the raw value is unsafe to
# 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.
# If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
# content returned by your JSON.
#
# If you need to output JSON elsewhere in your HTML, you can just do something
# like this, as any unsafe characters (including quotation marks) will be
# automatically escaped for you:
#
# <div data-user-info="<%= current_user.to_json %>">...</div>
#
# WARNING: this helper only works with valid JSON. Using this on non-JSON values
# will open up serious XSS vulnerabilities. For example, if you replace the
# +current_user.to_json+ in the example above with user input instead, the browser
# will happily eval() that string as JavaScript.
#
# The escaping performed in this method is identical to those performed in the
# Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
# set to true. Because this transformation is idempotent, this helper can be
# applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
#
# Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
# is enabled, or if you are unsure where your JSON string originated from, it
# is recommended that you always apply this helper (other libraries, such as the
# JSON gem, do not provide this kind of protection by default; also some gems
# might override +to_json+ to bypass Active Support's encoder).
- 1
def json_escape(s)
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
end
- 1
module_function :json_escape
end
end
- 1
class Object
- 1
def html_safe?
false
end
end
- 1
class Numeric
- 1
def html_safe?
true
end
end
- 1
module ActiveSupport #:nodoc:
- 1
class SafeBuffer < String
- 1
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete delete_prefix delete_suffix
downcase lstrip next reverse rstrip slice squeeze strip
succ swapcase tr tr_s unicode_normalize upcase
)
- 1
UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub)
- 1
alias_method :original_concat, :concat
- 1
private :original_concat
# Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers.
- 1
class SafeConcatError < StandardError
- 1
def initialize
super "Could not concatenate to the buffer because it is not html safe."
end
end
- 1
def [](*args)
if html_safe?
new_safe_buffer = super
if new_safe_buffer
new_safe_buffer.instance_variable_set :@html_safe, true
end
new_safe_buffer
else
to_str[*args]
end
end
- 1
def safe_concat(value)
raise SafeConcatError unless html_safe?
original_concat(value)
end
- 1
def initialize(str = "")
- 1
@html_safe = true
- 1
super
end
- 1
def initialize_copy(other)
super
@html_safe = other.html_safe?
end
- 1
def clone_empty
self[0, 0]
end
- 1
def concat(value)
super(html_escape_interpolated_argument(value))
end
- 1
alias << concat
- 1
def insert(index, value)
super(index, html_escape_interpolated_argument(value))
end
- 1
def prepend(value)
super(html_escape_interpolated_argument(value))
end
- 1
def replace(value)
super(html_escape_interpolated_argument(value))
end
- 1
def []=(*args)
if args.length == 3
super(args[0], args[1], html_escape_interpolated_argument(args[2]))
else
super(args[0], html_escape_interpolated_argument(args[1]))
end
end
- 1
def +(other)
dup.concat(other)
end
- 1
def *(*)
new_safe_buffer = super
new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
new_safe_buffer
end
- 1
def %(args)
case args
when Hash
escaped_args = args.transform_values { |arg| html_escape_interpolated_argument(arg) }
else
escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
end
self.class.new(super(escaped_args))
end
- 1
def html_safe?
defined?(@html_safe) && @html_safe
end
- 1
def to_s
self
end
- 1
def to_param
to_str
end
- 1
def encode_with(coder)
coder.represent_object nil, to_str
end
- 1
UNSAFE_STRING_METHODS.each do |unsafe_method|
- 20
if unsafe_method.respond_to?(unsafe_method)
- 20
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
end # end
def #{unsafe_method}!(*args) # def capitalize!(*args)
@html_safe = false # @html_safe = false
super # super
end # end
EOT
end
end
- 1
UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
- 2
if unsafe_method.respond_to?(unsafe_method)
- 2
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
if block # if block
to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
set_block_back_references(block, $~) # set_block_back_references(block, $~)
block.call(*params) # block.call(*params)
} # }
else # else
to_str.#{unsafe_method}(*args) # to_str.gsub(*args)
end # end
end # end
def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
@html_safe = false # @html_safe = false
if block # if block
super(*args) { |*params| # super(*args) { |*params|
set_block_back_references(block, $~) # set_block_back_references(block, $~)
block.call(*params) # block.call(*params)
} # }
else # else
super # super
end # end
end # end
EOT
end
end
- 1
private
- 1
def html_escape_interpolated_argument(arg)
(!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
end
- 1
def set_block_back_references(block, match_data)
block.binding.eval("proc { |m| $~ = m }").call(match_data)
rescue ArgumentError
# Can't create binding from C level Proc
end
end
end
- 1
class String
# Marks a string as trusted safe. It will be inserted into HTML with no
# additional escaping performed. It is your responsibility to ensure that the
# string contains no malicious content. This method is equivalent to the
# +raw+ helper in views. It is recommended that you use +sanitize+ instead of
# this method. It should never be called on user input.
- 1
def html_safe
ActiveSupport::SafeBuffer.new(self)
end
end
# frozen_string_literal: true
- 1
class String
- 1
alias :starts_with? :start_with?
- 1
alias :ends_with? :end_with?
end
# frozen_string_literal: true
- 1
class String
# Strips indentation in heredocs.
#
# For example in
#
# if options[:usage]
# puts <<-USAGE.strip_heredoc
# This command does such and such.
#
# Supported options are:
# -h This message
# ...
# USAGE
# end
#
# the user would see the usage message aligned against the left margin.
#
# Technically, it looks for the least indented non-empty line
# in the whole string, and removes that amount of leading whitespace.
- 1
def strip_heredoc
gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").tap do |stripped|
stripped.freeze if frozen?
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/string/conversions"
- 2
require "active_support/core_ext/time/zones"
- 2
class String
# Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
# is set, otherwise converts String to a Time via String#to_time
- 2
def in_time_zone(zone = ::Time.zone)
if zone
::Time.find_zone!(zone).parse(self)
else
to_time
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/symbol/starts_ends_with"
# frozen_string_literal: true
- 2
class Symbol
def start_with?(*prefixes)
- 152
to_s.start_with?(*prefixes)
- 2
end unless method_defined?(:start_with?)
def end_with?(*suffixes)
to_s.end_with?(*suffixes)
- 2
end unless method_defined?(:end_with?)
- 2
alias :starts_with? :start_with?
- 2
alias :ends_with? :end_with?
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/time/acts_like"
- 2
require "active_support/core_ext/time/calculations"
- 2
require "active_support/core_ext/time/compatibility"
- 2
require "active_support/core_ext/time/conversions"
- 2
require "active_support/core_ext/time/zones"
# frozen_string_literal: true
- 23
require "active_support/core_ext/object/acts_like"
- 23
class Time
# Duck-types as a Time-like class. See Object#acts_like?.
- 23
def acts_like_time?
true
end
end
# frozen_string_literal: true
- 23
require "active_support/duration"
- 23
require "active_support/core_ext/time/conversions"
- 23
require "active_support/time_with_zone"
- 23
require "active_support/core_ext/time/zones"
- 23
require "active_support/core_ext/date_and_time/calculations"
- 23
require "active_support/core_ext/date/calculations"
- 23
class Time
- 23
include DateAndTime::Calculations
- 23
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- 23
class << self
# Overriding case equality method so that it returns true for ActiveSupport::TimeWithZone instances
- 23
def ===(other)
- 1996
super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone))
end
# Returns the number of days in the given month.
# If no year is specified, it will use the current year.
- 23
def days_in_month(month, year = current.year)
if month == 2 && ::Date.gregorian_leap?(year)
29
else
COMMON_YEAR_DAYS_IN_MONTH[month]
end
end
# Returns the number of days in the given year.
# If no year is specified, it will use the current year.
- 23
def days_in_year(year = current.year)
days_in_month(2, year) + 337
end
# Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
- 23
def current
::Time.zone ? ::Time.zone.now : ::Time.now
end
# Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
# instances can be used when called with a single argument
- 23
def at_with_coercion(*args)
return at_without_coercion(*args) if args.size != 1
# Time.at can be called with a time or numerical value
time_or_number = args.first
if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
else
at_without_coercion(time_or_number)
end
end
- 23
alias_method :at_without_coercion, :at
- 23
alias_method :at, :at_with_coercion
# Creates a +Time+ instance from an RFC 3339 string.
#
# Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000
#
# If the time or offset components are missing then an +ArgumentError+ will be raised.
#
# Time.rfc3339('1999-12-31') # => ArgumentError: invalid date
- 23
def rfc3339(str)
parts = Date._rfc3339(str)
raise ArgumentError, "invalid date" if parts.empty?
Time.new(
parts.fetch(:year),
parts.fetch(:mon),
parts.fetch(:mday),
parts.fetch(:hour),
parts.fetch(:min),
parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
parts.fetch(:offset)
)
end
end
# Returns the number of seconds since 00:00:00.
#
# Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0.0
# Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296.0
# Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399.0
- 23
def seconds_since_midnight
to_i - change(hour: 0).to_i + (usec / 1.0e+6)
end
# Returns the number of seconds until 23:59:59.
#
# Time.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
# Time.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
# Time.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
- 23
def seconds_until_end_of_day
end_of_day.to_i - to_i
end
# Returns the fraction of a second as a +Rational+
#
# Time.new(2012, 8, 29, 0, 0, 0.5).sec_fraction # => (1/2)
- 23
def sec_fraction
subsec
end
# Returns a new Time where one or more of the elements have been changed according
# to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>,
# <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only
# the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
# and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter
# takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>,
# <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>,
# <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
#
# Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0)
- 23
def change(options)
new_year = options.fetch(:year, year)
new_month = options.fetch(:month, month)
new_day = options.fetch(:day, day)
new_hour = options.fetch(:hour, hour)
new_min = options.fetch(:min, options[:hour] ? 0 : min)
new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
new_offset = options.fetch(:offset, nil)
if new_nsec = options[:nsec]
raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
new_usec = Rational(new_nsec, 1000)
else
new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
end
raise ArgumentError, "argument out of range" if new_usec >= 1000000
new_sec += Rational(new_usec, 1000000)
if new_offset
::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset)
elsif utc?
::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec)
elsif zone
::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec)
else
::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset)
end
end
# Uses Date to provide precise Time calculations for years, months, and days
# according to the proleptic Gregorian calendar. The +options+ parameter
# takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
# <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
# <tt>:seconds</tt>.
#
# Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700
# Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700
# Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700
# Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700
# Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700
- 23
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
end
unless options[:days].nil?
options[:days], partial_days = options[:days].divmod(1)
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
d = to_date.gregorian.advance(options)
time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
options.fetch(:hours, 0) * 3600
if seconds_to_advance.zero?
time_advanced_by_date
else
time_advanced_by_date.since(seconds_to_advance)
end
end
# Returns a new Time representing the time a number of seconds ago, this is basically a wrapper around the Numeric extension
- 23
def ago(seconds)
since(-seconds)
end
# Returns a new Time representing the time a number of seconds since the instance time
- 23
def since(seconds)
self + seconds
rescue
to_datetime.since(seconds)
end
- 23
alias :in :since
# Returns a new Time representing the start of the day (0:00)
- 23
def beginning_of_day
change(hour: 0)
end
- 23
alias :midnight :beginning_of_day
- 23
alias :at_midnight :beginning_of_day
- 23
alias :at_beginning_of_day :beginning_of_day
# Returns a new Time representing the middle of the day (12:00)
- 23
def middle_of_day
change(hour: 12)
end
- 23
alias :midday :middle_of_day
- 23
alias :noon :middle_of_day
- 23
alias :at_midday :middle_of_day
- 23
alias :at_noon :middle_of_day
- 23
alias :at_middle_of_day :middle_of_day
# Returns a new Time representing the end of the day, 23:59:59.999999
- 23
def end_of_day
change(
hour: 23,
min: 59,
sec: 59,
usec: Rational(999999999, 1000)
)
end
- 23
alias :at_end_of_day :end_of_day
# Returns a new Time representing the start of the hour (x:00)
- 23
def beginning_of_hour
change(min: 0)
end
- 23
alias :at_beginning_of_hour :beginning_of_hour
# Returns a new Time representing the end of the hour, x:59:59.999999
- 23
def end_of_hour
change(
min: 59,
sec: 59,
usec: Rational(999999999, 1000)
)
end
- 23
alias :at_end_of_hour :end_of_hour
# Returns a new Time representing the start of the minute (x:xx:00)
- 23
def beginning_of_minute
change(sec: 0)
end
- 23
alias :at_beginning_of_minute :beginning_of_minute
# Returns a new Time representing the end of the minute, x:xx:59.999999
- 23
def end_of_minute
change(
sec: 59,
usec: Rational(999999999, 1000)
)
end
- 23
alias :at_end_of_minute :end_of_minute
- 23
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
- 23
alias_method :plus_without_duration, :+
- 23
alias_method :+, :plus_with_duration
- 23
def minus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.until(self)
else
minus_without_duration(other)
end
end
- 23
alias_method :minus_without_duration, :-
- 23
alias_method :-, :minus_with_duration
# Time#- can also be used to determine the number of seconds between two Time instances.
# We're layering on additional behavior so that ActiveSupport::TimeWithZone instances
# are coerced into values that Time#- will recognize
- 23
def minus_with_coercion(other)
other = other.comparable_time if other.respond_to?(:comparable_time)
other.is_a?(DateTime) ? to_f - other.to_f : minus_without_coercion(other)
end
- 23
alias_method :minus_without_coercion, :-
- 23
alias_method :-, :minus_with_coercion
# Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
# can be chronologically compared with a Time
- 23
def compare_with_coercion(other)
# we're avoiding Time#to_datetime and Time#to_time because they're expensive
if other.class == Time
compare_without_coercion(other)
elsif other.is_a?(Time)
compare_without_coercion(other.to_time)
else
to_datetime <=> other
end
end
- 23
alias_method :compare_without_coercion, :<=>
- 23
alias_method :<=>, :compare_with_coercion
# Layers additional behavior on Time#eql? so that ActiveSupport::TimeWithZone instances
# can be eql? to an equivalent Time
- 23
def eql_with_coercion(other)
# if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do eql? comparison
other = other.comparable_time if other.respond_to?(:comparable_time)
eql_without_coercion(other)
end
- 23
alias_method :eql_without_coercion, :eql?
- 23
alias_method :eql?, :eql_with_coercion
# Returns a new time the specified number of days ago.
- 23
def prev_day(days = 1)
advance(days: -days)
end
# Returns a new time the specified number of days in the future.
- 23
def next_day(days = 1)
advance(days: days)
end
# Returns a new time the specified number of months ago.
- 23
def prev_month(months = 1)
advance(months: -months)
end
# Returns a new time the specified number of months in the future.
- 23
def next_month(months = 1)
advance(months: months)
end
# Returns a new time the specified number of years ago.
- 23
def prev_year(years = 1)
advance(years: -years)
end
# Returns a new time the specified number of years in the future.
- 23
def next_year(years = 1)
advance(years: years)
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/date_and_time/compatibility"
- 2
require "active_support/core_ext/module/redefine_method"
- 2
class Time
- 2
include DateAndTime::Compatibility
- 2
silence_redefinition_of_method :to_time
# Either return +self+ or the time in the local system timezone depending
# on the setting of +ActiveSupport.to_time_preserves_timezone+.
- 2
def to_time
preserve_timezone ? self : getlocal
end
end
# frozen_string_literal: true
- 23
require "active_support/inflector/methods"
- 23
require "active_support/values/time_zone"
- 23
class Time
- 23
DATE_FORMATS = {
db: "%Y-%m-%d %H:%M:%S",
inspect: "%Y-%m-%d %H:%M:%S.%9N %z",
number: "%Y%m%d%H%M%S",
nsec: "%Y%m%d%H%M%S%9N",
usec: "%Y%m%d%H%M%S%6N",
time: "%H:%M",
short: "%d %b %H:%M",
long: "%B %d, %Y %H:%M",
long_ordinal: lambda { |time|
day_format = ActiveSupport::Inflector.ordinalize(time.day)
time.strftime("%B #{day_format}, %Y %H:%M")
},
rfc822: lambda { |time|
offset_format = time.formatted_offset(false)
time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
},
iso8601: lambda { |time| time.iso8601 }
}
# Converts to a formatted string. See DATE_FORMATS for built-in formats.
#
# This method is aliased to <tt>to_s</tt>.
#
# time = Time.now # => 2007-01-18 06:10:17 -06:00
#
# time.to_formatted_s(:time) # => "06:10"
# time.to_s(:time) # => "06:10"
#
# time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
# time.to_formatted_s(:number) # => "20070118061017"
# time.to_formatted_s(:short) # => "18 Jan 06:10"
# time.to_formatted_s(:long) # => "January 18, 2007 06:10"
# time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
# time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
# time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00"
#
# == Adding your own time formats to +to_formatted_s+
# You can add your own formats to the Time::DATE_FORMATS hash.
# Use the format name as the hash key and either a strftime string
# or Proc instance that takes a time argument as the value.
#
# # config/initializers/time_formats.rb
# Time::DATE_FORMATS[:month_and_year] = '%B %Y'
# Time::DATE_FORMATS[:short_ordinal] = ->(time) { time.strftime("%B #{time.day.ordinalize}") }
- 23
def to_formatted_s(format = :default)
if formatter = DATE_FORMATS[format]
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
else
to_default_s
end
end
- 23
alias_method :to_default_s, :to_s
- 23
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.
#
# Time.local(2000).formatted_offset # => "-06:00"
# Time.local(2000).formatted_offset(false) # => "-0600"
- 23
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
# Aliased to +xmlschema+ for compatibility with +DateTime+
- 23
alias_method :rfc3339, :xmlschema
end
# frozen_string_literal: true
- 23
require "active_support/time_with_zone"
- 23
require "active_support/core_ext/time/acts_like"
- 23
require "active_support/core_ext/date_and_time/zones"
- 23
class Time
- 23
include DateAndTime::Zones
- 23
class << self
- 23
attr_accessor :zone_default
# Returns the TimeZone for the current request, if this has been set (via Time.zone=).
# If <tt>Time.zone</tt> has not been set for the current request, returns the TimeZone specified in <tt>config.time_zone</tt>.
- 23
def zone
Thread.current[:time_zone] || zone_default
end
# Sets <tt>Time.zone</tt> to a TimeZone object for the current request/thread.
#
# This method accepts any of the following:
#
# * A Rails TimeZone object.
# * An identifier for a Rails TimeZone object (e.g., "Eastern Time (US & Canada)", <tt>-5.hours</tt>).
# * A TZInfo::Timezone object.
# * An identifier for a TZInfo::Timezone object (e.g., "America/New_York").
#
# Here's an example of how you might set <tt>Time.zone</tt> on a per request basis and reset it when the request is done.
# <tt>current_user.time_zone</tt> just needs to return a string identifying the user's preferred time zone:
#
# class ApplicationController < ActionController::Base
# around_action :set_time_zone
#
# def set_time_zone
# if logged_in?
# Time.use_zone(current_user.time_zone) { yield }
# else
# yield
# end
# end
# end
- 23
def zone=(time_zone)
Thread.current[:time_zone] = find_zone!(time_zone)
end
# Allows override of <tt>Time.zone</tt> locally inside supplied block;
# resets <tt>Time.zone</tt> to existing value when done.
#
# class ApplicationController < ActionController::Base
# around_action :set_time_zone
#
# private
#
# def set_time_zone
# Time.use_zone(current_user.timezone) { yield }
# end
# end
#
# NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
# objects that have already been created, e.g. any model timestamp
# attributes that have been read before the block will remain in
# the application's default timezone.
- 23
def use_zone(time_zone)
new_zone = find_zone!(time_zone)
begin
old_zone, ::Time.zone = ::Time.zone, new_zone
yield
ensure
::Time.zone = old_zone
end
end
# Returns a TimeZone instance matching the time zone provided.
# Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
# Raises an +ArgumentError+ for invalid time zones.
#
# Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
# Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...>
# Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...>
# Time.find_zone! nil # => nil
# Time.find_zone! false # => false
# Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE
- 23
def find_zone!(time_zone)
if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone)
time_zone
else
# Look up the timezone based on the identifier (unless we've been
# passed a TZInfo::Timezone)
unless time_zone.respond_to?(:period_for_local)
time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone)
end
# Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone
if time_zone.is_a?(ActiveSupport::TimeZone)
time_zone
else
ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone)
end
end
rescue TZInfo::InvalidTimezoneIdentifier
raise ArgumentError, "Invalid Timezone: #{time_zone}"
end
# Returns a TimeZone instance matching the time zone provided.
# Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
# Returns +nil+ for invalid time zones.
#
# Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
# Time.find_zone "NOT-A-TIMEZONE" # => nil
- 23
def find_zone(time_zone)
find_zone!(time_zone) rescue nil
end
end
end
# frozen_string_literal: true
- 1
require "uri"
- 1
if RUBY_VERSION < "2.6.0"
- 1
require "active_support/core_ext/module/redefine_method"
- 1
URI::Parser.class_eval do
- 1
silence_redefinition_of_method :unescape
- 1
def unescape(str, escaped = /%[a-fA-F\d]{2}/)
# TODO: Are we actually sure that ASCII == UTF-8?
# YK: My initial experiments say yes, but let's be sure please
enc = str.encoding
enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc)
end
end
end
- 1
module URI
- 1
class << self
- 1
def parser
ActiveSupport::Deprecation.warn(<<-MSG.squish)
URI.parser is deprecated and will be removed in Rails 6.2.
Use `URI::DEFAULT_PARSER` instead.
MSG
URI::DEFAULT_PARSER
end
end
end
# frozen_string_literal: true
- 1
require "active_support/callbacks"
- 1
require "active_support/core_ext/enumerable"
- 1
module ActiveSupport
# Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
# before and after each request. This allows you to keep all the per-request attributes easily
# available to the whole system.
#
# The following full app-like example demonstrates how to use a Current class to
# facilitate easy access to the global, per-request attributes without passing them deeply
# around everywhere:
#
# # app/models/current.rb
# class Current < ActiveSupport::CurrentAttributes
# attribute :account, :user
# attribute :request_id, :user_agent, :ip_address
#
# resets { Time.zone = nil }
#
# def user=(user)
# super
# self.account = user.account
# Time.zone = user.time_zone
# end
# end
#
# # app/controllers/concerns/authentication.rb
# module Authentication
# extend ActiveSupport::Concern
#
# included do
# before_action :authenticate
# end
#
# private
# def authenticate
# if authenticated_user = User.find_by(id: cookies.encrypted[:user_id])
# Current.user = authenticated_user
# else
# redirect_to new_session_url
# end
# end
# end
#
# # app/controllers/concerns/set_current_request_details.rb
# module SetCurrentRequestDetails
# extend ActiveSupport::Concern
#
# included do
# before_action do
# Current.request_id = request.uuid
# Current.user_agent = request.user_agent
# Current.ip_address = request.ip
# end
# end
# end
#
# class ApplicationController < ActionController::Base
# include Authentication
# include SetCurrentRequestDetails
# end
#
# class MessagesController < ApplicationController
# def create
# Current.account.messages.create(message_params)
# end
# end
#
# class Message < ApplicationRecord
# belongs_to :creator, default: -> { Current.user }
# after_create { |message| Event.create(record: message) }
# end
#
# class Event < ApplicationRecord
# before_create do
# self.request_id = Current.request_id
# self.user_agent = Current.user_agent
# self.ip_address = Current.ip_address
# end
# end
#
# A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
# Current should only be used for a few, top-level globals, like account, user, and request details.
# The attributes stuck in Current should be used by more or less all actions on all requests. If you start
# sticking controller-specific attributes in there, you're going to create a mess.
- 1
class CurrentAttributes
- 1
include ActiveSupport::Callbacks
- 1
define_callbacks :reset
- 1
class << self
# Returns singleton instance for this class in this thread. If none exists, one is created.
- 1
def instance
- 2
current_instances[current_instances_key] ||= new
end
# Declares one or more attributes that will be given both class and instance accessor methods.
- 1
def attribute(*names)
- 2
generated_attribute_methods.module_eval do
- 2
names.each do |name|
- 6
define_method(name) do
attributes[name.to_sym]
end
- 6
define_method("#{name}=") do |attribute|
attributes[name.to_sym] = attribute
end
end
end
- 2
names.each do |name|
- 6
define_singleton_method(name) do
instance.public_send(name)
end
- 6
define_singleton_method("#{name}=") do |attribute|
instance.public_send("#{name}=", attribute)
end
end
end
# Calls this block before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
- 1
def before_reset(&block)
- 1
set_callback :reset, :before, &block
end
# Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
- 1
def resets(&block)
- 1
set_callback :reset, :after, &block
end
- 1
alias_method :after_reset, :resets
- 1
delegate :set, :reset, to: :instance
- 1
def reset_all # :nodoc:
current_instances.each_value(&:reset)
end
- 1
def clear_all # :nodoc:
reset_all
current_instances.clear
end
- 1
private
- 1
def generated_attribute_methods
- 4
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
- 1
def current_instances
- 2
Thread.current[:current_attributes_instances] ||= {}
end
- 1
def current_instances_key
- 2
@current_instances_key ||= name.to_sym
end
- 1
def method_missing(name, *args, &block)
# Caches the method definition as a singleton method of the receiver.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
singleton_class.delegate name, to: :instance
send(name, *args, &block)
end
end
- 1
attr_accessor :attributes
- 1
def initialize
- 2
@attributes = {}
end
# Expose one or more attributes within a block. Old values are returned after the block concludes.
# Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
#
# class Chat::PublicationJob < ApplicationJob
# def perform(attributes, room_number, creator)
# Current.set(person: creator) do
# Chat::Publisher.publish(attributes: attributes, room_number: room_number)
# end
# end
# end
- 1
def set(set_attributes)
old_attributes = compute_attributes(set_attributes.keys)
assign_attributes(set_attributes)
yield
ensure
assign_attributes(old_attributes)
end
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
- 1
def reset
run_callbacks :reset do
self.attributes = {}
end
end
- 1
private
- 1
def assign_attributes(new_attributes)
new_attributes.each { |key, value| public_send("#{key}=", value) }
end
- 1
def compute_attributes(keys)
keys.index_with { |key| public_send(key) }
end
end
end
# frozen_string_literal: true
- 1
module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
- 1
def before_setup
ActiveSupport::CurrentAttributes.reset_all
super
end
- 1
def before_teardown
ActiveSupport::CurrentAttributes.reset_all
super
end
end
# frozen_string_literal: true
- 3
require "set"
- 3
require "thread"
- 3
require "concurrent/map"
- 3
require "pathname"
- 3
require "active_support/core_ext/module/aliasing"
- 3
require "active_support/core_ext/module/attribute_accessors"
- 3
require "active_support/core_ext/module/introspection"
- 3
require "active_support/core_ext/module/anonymous"
- 3
require "active_support/core_ext/object/blank"
- 3
require "active_support/core_ext/kernel/reporting"
- 3
require "active_support/core_ext/load_error"
- 3
require "active_support/core_ext/name_error"
- 3
require "active_support/dependencies/interlock"
- 3
require "active_support/inflector"
- 3
module ActiveSupport #:nodoc:
- 3
module Dependencies #:nodoc:
- 3
extend self
- 3
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
- 3
private_constant :UNBOUND_METHOD_MODULE_NAME
- 3
mattr_accessor :interlock, default: Interlock.new
# :doc:
# Execute the supplied block without interference from any
# concurrent loads.
- 3
def self.run_interlock
Dependencies.interlock.running { yield }
end
# Execute the supplied block while holding an exclusive lock,
# preventing any other thread from being inside a #run_interlock
# block at the same time.
- 3
def self.load_interlock
Dependencies.interlock.loading { yield }
end
# Execute the supplied block while holding an exclusive lock,
# preventing any other thread from being inside a #run_interlock
# block at the same time.
- 3
def self.unload_interlock
Dependencies.interlock.unloading { yield }
end
# :nodoc:
# Should we turn on Ruby warnings on the first load of dependent files?
- 3
mattr_accessor :warnings_on_first_load, default: false
# All files ever loaded.
- 3
mattr_accessor :history, default: Set.new
# All files currently loaded.
- 3
mattr_accessor :loaded, default: Set.new
# Stack of files being loaded.
- 3
mattr_accessor :loading, default: []
# Should we load files or require them?
- 3
mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load
# The set of directories from which we may automatically load files. Files
# under these directories will be reloaded on each request in development mode,
# unless the directory also appears in autoload_once_paths.
- 3
mattr_accessor :autoload_paths, default: []
# The set of directories from which automatically loaded constants are loaded
# only once. All directories in this set must also be present in +autoload_paths+.
- 3
mattr_accessor :autoload_once_paths, default: []
# This is a private set that collects all eager load paths during bootstrap.
# Useful for Zeitwerk integration. Its public interface is the config.* path
# accessors of each engine.
- 3
mattr_accessor :_eager_load_paths, default: Set.new
# An array of qualified constant names that have been loaded. Adding a name
# to this array will cause it to be unloaded the next time Dependencies are
# cleared.
- 3
mattr_accessor :autoloaded_constants, default: []
# An array of constant names that need to be unloaded on every request. Used
# to allow arbitrary constants to be marked for unloading.
- 3
mattr_accessor :explicitly_unloadable_constants, default: []
# The logger used when tracing autoloads.
- 3
mattr_accessor :logger
# If true, trace autoloads with +logger.debug+.
- 3
mattr_accessor :verbose, default: false
# The WatchStack keeps a stack of the modules being watched as files are
# loaded. If a file in the process of being loaded (parent.rb) triggers the
# load of another file (child.rb) the stack will ensure that child.rb
# handles the new constants.
#
# If child.rb is being autoloaded, its constants will be added to
# autoloaded_constants. If it was being required, they will be discarded.
#
# This is handled by walking back up the watch stack and adding the constants
# found by child.rb to the list of original constants in parent.rb.
- 3
class WatchStack
- 3
include Enumerable
# @watching is a stack of lists of constants being watched. For instance,
# if parent.rb is autoloaded, the stack will look like [[Object]]. If
# parent.rb then requires namespace/child.rb, the stack will look like
# [[Object], [Namespace]].
- 3
attr_reader :watching
- 3
def initialize
- 3
@watching = []
- 3
@stack = Hash.new { |h, k| h[k] = [] }
end
- 3
def each(&block)
@stack.each(&block)
end
- 3
def watching?
!@watching.empty?
end
# Returns a list of new constants found since the last call to
# <tt>watch_namespaces</tt>.
- 3
def new_constants
constants = []
# Grab the list of namespaces that we're looking for new constants under
@watching.last.each do |namespace|
# Retrieve the constants that were present under the namespace when watch_namespaces
# was originally called
original_constants = @stack[namespace].last
mod = Inflector.constantize(namespace) if Dependencies.qualified_const_defined?(namespace)
next unless mod.is_a?(Module)
# Get a list of the constants that were added
new_constants = mod.constants(false) - original_constants
# @stack[namespace] returns an Array of the constants that are being evaluated
# for that namespace. For instance, if parent.rb requires child.rb, the first
# element of @stack[Object] will be an Array of the constants that were present
# before parent.rb was required. The second element will be an Array of the
# constants that were present before child.rb was required.
@stack[namespace].each do |namespace_constants|
namespace_constants.concat(new_constants)
end
# Normalize the list of new constants, and add them to the list we will return
new_constants.each do |suffix|
constants << ([namespace, suffix] - ["Object"]).join("::")
end
end
constants
ensure
# A call to new_constants is always called after a call to watch_namespaces
pop_modules(@watching.pop)
end
# Add a set of modules to the watch stack, remembering the initial
# constants.
- 3
def watch_namespaces(namespaces)
@watching << namespaces.map do |namespace|
module_name = Dependencies.to_constant_name(namespace)
original_constants = Dependencies.qualified_const_defined?(module_name) ?
Inflector.constantize(module_name).constants(false) : []
@stack[module_name] << original_constants
module_name
end
end
- 3
private
- 3
def pop_modules(modules)
modules.each { |mod| @stack[mod].pop }
end
end
# An internal stack used to record which constants are loaded by any block.
- 3
mattr_accessor :constant_watch_stack, default: WatchStack.new
# Module includes this module.
- 3
module ModuleConstMissing #:nodoc:
- 3
def self.append_features(base)
- 6
base.class_eval do
# Emulate #exclude via an ivar
- 6
return if defined?(@_const_missing) && @_const_missing
- 3
@_const_missing = instance_method(:const_missing)
- 3
remove_method(:const_missing)
end
- 3
super
end
- 3
def self.exclude_from(base)
base.class_eval do
define_method :const_missing, @_const_missing
@_const_missing = nil
end
end
- 3
def self.include_into(base)
- 3
base.include(self)
- 3
append_features(base)
end
- 3
def const_missing(const_name)
from_mod = anonymous? ? guess_for_anonymous(const_name) : self
Dependencies.load_missing_constant(from_mod, const_name)
end
# We assume that the name of the module reflects the nesting
# (unless it can be proven that is not the case) and the path to the file
# that defines the constant. Anonymous modules cannot follow these
# conventions and therefore we assume that the user wants to refer to a
# top-level constant.
- 3
def guess_for_anonymous(const_name)
if Object.const_defined?(const_name)
raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
else
Object
end
end
- 3
def unloadable(const_desc = self)
super(const_desc)
end
end
# Object includes this module.
- 3
module Loadable #:nodoc:
- 3
def self.exclude_from(base)
base.class_eval do
define_method(:load, Kernel.instance_method(:load))
private :load
define_method(:require, Kernel.instance_method(:require))
private :require
end
end
- 3
def self.include_into(base)
- 3
base.include(self)
- 3
if base.instance_method(:load).owner == base
base.remove_method(:load)
end
- 3
if base.instance_method(:require).owner == base
base.remove_method(:require)
end
end
- 3
def require_or_load(file_name)
Dependencies.require_or_load(file_name)
end
# :doc:
# <b>Warning:</b> This method is obsolete in +:zeitwerk+ mode. In
# +:zeitwerk+ mode semantics match Ruby's and you do not need to be
# defensive with load order. Just refer to classes and modules normally.
# If the constant name is dynamic, camelize if needed, and constantize.
#
# In +:classic+ mode, interprets a file using +mechanism+ and marks its
# defined constants as autoloaded. +file_name+ can be either a string or
# respond to <tt>to_path</tt>.
#
# In +:classic+ mode, use this method in code that absolutely needs a
# certain constant to be defined at that point. A typical use case is to
# make constant name resolution deterministic for constants with the same
# relative name in different namespaces whose evaluation would depend on
# load order otherwise.
#
# Engines that do not control the mode in which their parent application
# runs should call +require_dependency+ where needed in case the runtime
# mode is +:classic+.
- 3
def require_dependency(file_name, message = "No such file to load -- %s.rb")
file_name = file_name.to_path if file_name.respond_to?(:to_path)
unless file_name.is_a?(String)
raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
end
Dependencies.depend_on(file_name, message)
end
# :nodoc:
- 3
def load_dependency(file)
- 767
if Dependencies.load? && Dependencies.constant_watch_stack.watching?
descs = Dependencies.constant_watch_stack.watching.flatten.uniq
Dependencies.new_constants_in(*descs) { yield }
else
- 767
yield
end
rescue Exception => exception # errors from loading file
- 4
exception.blame_file! file if exception.respond_to? :blame_file!
- 4
raise
end
# Mark the given constant as unloadable. Unloadable constants are removed
# each time dependencies are cleared.
#
# Note that marking a constant for unloading need only be done once. Setup
# or init scripts may list each unloadable constant that may need unloading;
# each constant will be removed for every subsequent clear, as opposed to
# for the first clear.
#
# The provided constant descriptor may be a (non-anonymous) module or class,
# or a qualified constant name as a string or symbol.
#
# Returns +true+ if the constant was not previously marked for unloading,
# +false+ otherwise.
- 3
def unloadable(const_desc)
Dependencies.mark_for_unload const_desc
end
- 3
private
- 3
def load(file, wrap = false)
result = false
load_dependency(file) { result = super }
result
end
- 3
def require(file)
- 767
result = false
- 1534
load_dependency(file) { result = super }
- 763
result
end
end
# Exception file-blaming.
- 3
module Blamable #:nodoc:
- 3
def blame_file!(file)
- 4
(@blamed_files ||= []).unshift file
end
- 3
def blamed_files
@blamed_files ||= []
end
- 3
def describe_blame
return nil if blamed_files.empty?
"This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
end
- 3
def copy_blame!(exc)
@blamed_files = exc.blamed_files.clone
self
end
end
- 3
def hook!
- 3
Loadable.include_into(Object)
- 3
ModuleConstMissing.include_into(Module)
- 3
Exception.include(Blamable)
end
- 3
def unhook!
ModuleConstMissing.exclude_from(Module)
Loadable.exclude_from(Object)
end
- 3
def load?
- 767
mechanism == :load
end
- 3
def depend_on(file_name, message = "No such file to load -- %s.rb")
path = search_for_file(file_name)
require_or_load(path || file_name)
rescue LoadError => load_error
if file_name = load_error.message[/ -- (.*?)(\.rb)?$/, 1]
load_error.message.replace(message % file_name)
load_error.copy_blame!(load_error)
end
raise
end
- 3
def clear
Dependencies.unload_interlock do
loaded.clear
loading.clear
remove_unloadable_constants!
end
end
- 3
def require_or_load(file_name, const_path = nil)
file_name = file_name.chomp(".rb")
expanded = File.expand_path(file_name)
return if loaded.include?(expanded)
Dependencies.load_interlock do
# Maybe it got loaded while we were waiting for our lock:
return if loaded.include?(expanded)
# Record that we've seen this file *before* loading it to avoid an
# infinite loop with mutual dependencies.
loaded << expanded
loading << expanded
begin
if load?
# Enable warnings if this file has not been loaded before and
# warnings_on_first_load is set.
load_args = ["#{file_name}.rb"]
load_args << const_path unless const_path.nil?
if !warnings_on_first_load || history.include?(expanded)
result = load_file(*load_args)
else
enable_warnings { result = load_file(*load_args) }
end
else
result = require file_name
end
rescue Exception
loaded.delete expanded
raise
ensure
loading.pop
end
# Record history *after* loading so first load gets warnings.
history << expanded
result
end
end
# Is the provided constant path defined?
- 3
def qualified_const_defined?(path)
Object.const_defined?(path, false)
end
# Given +path+, a filesystem path to a ruby file, return an array of
# constant paths which would cause Dependencies to attempt to load this
# file.
- 3
def loadable_constants_for_path(path, bases = autoload_paths)
path = path.chomp(".rb")
expanded_path = File.expand_path(path)
paths = []
bases.each do |root|
expanded_root = File.expand_path(root)
next unless expanded_path.start_with?(expanded_root)
root_size = expanded_root.size
next if expanded_path[root_size] != ?/
nesting = expanded_path[(root_size + 1)..-1]
paths << nesting.camelize unless nesting.blank?
end
paths.uniq!
paths
end
# Search for a file in autoload_paths matching the provided suffix.
- 3
def search_for_file(path_suffix)
path_suffix += ".rb" unless path_suffix.end_with?(".rb")
autoload_paths.each do |root|
path = File.join(root, path_suffix)
return path if File.file? path
end
nil # Gee, I sure wish we had first_match ;-)
end
# Does the provided path_suffix correspond to an autoloadable module?
# Instead of returning a boolean, the autoload base for this module is
# returned.
- 3
def autoloadable_module?(path_suffix)
autoload_paths.each do |load_path|
return load_path if File.directory? File.join(load_path, path_suffix)
end
nil
end
- 3
def load_once_path?(path)
# to_s works around a ruby issue where String#start_with?(Pathname)
# will raise a TypeError: no implicit conversion of Pathname into String
autoload_once_paths.any? { |base| path.start_with?(base.to_s) }
end
# Attempt to autoload the provided module name by searching for a directory
# matching the expected path suffix. If found, the module is created and
# assigned to +into+'s constants with the name +const_name+. Provided that
# the directory was loaded from a reloadable base path, it is added to the
# set of constants that are to be unloaded.
- 3
def autoload_module!(into, const_name, qualified_name, path_suffix)
return nil unless base_path = autoloadable_module?(path_suffix)
mod = Module.new
into.const_set const_name, mod
log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})")
autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
autoloaded_constants.uniq!
mod
end
# Load the file at the provided path. +const_paths+ is a set of qualified
# constant names. When loading the file, Dependencies will watch for the
# addition of these constants. Each that is defined will be marked as
# autoloaded, and will be removed when Dependencies.clear is next called.
#
# If the second parameter is left off, then Dependencies will construct a
# set of names that the file at +path+ may define. See
# +loadable_constants_for_path+ for more details.
- 3
def load_file(path, const_paths = loadable_constants_for_path(path))
const_paths = [const_paths].compact unless const_paths.is_a? Array
parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
result = nil
newly_defined_paths = new_constants_in(*parent_paths) do
result = Kernel.load path
end
autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
autoloaded_constants.uniq!
result
end
# Returns the constant path for the provided parent and constant name.
- 3
def qualified_name_for(mod, name)
mod_name = to_constant_name mod
mod_name == "Object" ? name.to_s : "#{mod_name}::#{name}"
end
# Load the constant named +const_name+ which is missing from +from_mod+. If
# it is not possible to load the constant into from_mod, try its parent
# module using +const_missing+.
- 3
def load_missing_constant(from_mod, const_name)
unless qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod)
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
qualified_name = qualified_name_for(from_mod, const_name)
path_suffix = qualified_name.underscore
file_path = search_for_file(path_suffix)
if file_path
expanded = File.expand_path(file_path)
expanded.delete_suffix!(".rb")
if loading.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
else
require_or_load(expanded, qualified_name)
if from_mod.const_defined?(const_name, false)
log("constant #{qualified_name} autoloaded from #{expanded}.rb")
return from_mod.const_get(const_name)
else
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it"
end
end
elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
return mod
elsif (parent = from_mod.module_parent) && parent != from_mod &&
! from_mod.module_parents.any? { |p| p.const_defined?(const_name, false) }
# If our parents do not have a constant named +const_name+ then we are free
# to attempt to load upwards. If they do have such a constant, then this
# const_missing must be due to from_mod::const_name, which should not
# return constants from from_mod's parents.
begin
# Since Ruby does not pass the nesting at the point the unknown
# constant triggered the callback we cannot fully emulate constant
# name lookup and need to make a trade-off: we are going to assume
# that the nesting in the body of Foo::Bar is [Foo::Bar, Foo] even
# though it might not be. Counterexamples are
#
# class Foo::Bar
# Module.nesting # => [Foo::Bar]
# end
#
# or
#
# module M::N
# module S::T
# Module.nesting # => [S::T, M::N]
# end
# end
#
# for example.
return parent.const_missing(const_name)
rescue NameError => e
raise unless e.missing_name? qualified_name_for(parent, const_name)
end
end
name_error = uninitialized_constant(qualified_name, const_name, receiver: from_mod)
name_error.set_backtrace(caller.reject { |l| l.start_with? __FILE__ })
raise name_error
end
# Remove the constants that have been autoloaded, and those that have been
# marked for unloading. Before each constant is removed a callback is sent
# to its class/module if it implements +before_remove_const+.
#
# The callback implementation should be restricted to cleaning up caches, etc.
# as the environment will be in an inconsistent state, e.g. other constants
# may have already been unloaded and not accessible.
- 3
def remove_unloadable_constants!
log("removing unloadable constants")
autoloaded_constants.each { |const| remove_constant const }
autoloaded_constants.clear
Reference.clear!
explicitly_unloadable_constants.each { |const| remove_constant const }
end
- 3
class ClassCache
- 3
def initialize
- 3
@store = Concurrent::Map.new
end
- 3
def empty?
@store.empty?
end
- 3
def key?(key)
@store.key?(key)
end
- 3
def get(key)
key = key.name if key.respond_to?(:name)
@store[key] ||= Inflector.constantize(key)
end
- 3
alias :[] :get
- 3
def safe_get(key)
key = key.name if key.respond_to?(:name)
@store[key] ||= Inflector.safe_constantize(key)
end
- 3
def store(klass)
return self unless klass.respond_to?(:name)
raise(ArgumentError, "anonymous classes cannot be cached") if klass.name.empty?
@store[klass.name] = klass
self
end
- 3
def clear!
@store.clear
end
end
- 3
Reference = ClassCache.new
# Store a reference to a class +klass+.
- 3
def reference(klass)
Reference.store klass
end
# Get the reference for class named +name+.
# Raises an exception if referenced class does not exist.
- 3
def constantize(name)
Reference.get(name)
end
# Get the reference for class named +name+ if one exists.
# Otherwise returns +nil+.
- 3
def safe_constantize(name)
Reference.safe_get(name)
end
# Determine if the given constant has been automatically loaded.
- 3
def autoloaded?(desc)
return false if desc.is_a?(Module) && real_mod_name(desc).nil?
name = to_constant_name desc
return false unless qualified_const_defined?(name)
autoloaded_constants.include?(name)
end
# Will the provided constant descriptor be unloaded?
- 3
def will_unload?(const_desc)
autoloaded?(const_desc) ||
explicitly_unloadable_constants.include?(to_constant_name(const_desc))
end
# Mark the provided constant name for unloading. This constant will be
# unloaded on each request, not just the next one.
- 3
def mark_for_unload(const_desc)
name = to_constant_name const_desc
if explicitly_unloadable_constants.include? name
false
else
explicitly_unloadable_constants << name
true
end
end
# Run the provided block and detect the new constants that were loaded during
# its execution. Constants may only be regarded as 'new' once -- so if the
# block calls +new_constants_in+ again, then the constants defined within the
# inner call will not be reported in this one.
#
# If the provided block does not run to completion, and instead raises an
# exception, any new constants are regarded as being only partially defined
# and will be removed immediately.
- 3
def new_constants_in(*descs)
constant_watch_stack.watch_namespaces(descs)
success = false
begin
yield # Now yield to the code that is to define new constants.
success = true
ensure
new_constants = constant_watch_stack.new_constants
return new_constants if success
# Remove partially loaded constants.
new_constants.each { |c| remove_constant(c) }
end
end
# Convert the provided const desc to a qualified constant name (as a string).
# A module, class, symbol, or string may be provided.
- 3
def to_constant_name(desc) #:nodoc:
case desc
when String then desc.delete_prefix("::")
when Symbol then desc.to_s
when Module
real_mod_name(desc) ||
raise(ArgumentError, "Anonymous modules have no name to be referenced by")
else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
end
end
- 3
def remove_constant(const) #:nodoc:
# Normalize ::Foo, ::Object::Foo, Object::Foo, Object::Object::Foo, etc. as Foo.
normalized = const.to_s.delete_prefix("::")
normalized.sub!(/\A(Object::)+/, "")
constants = normalized.split("::")
to_remove = constants.pop
# Remove the file path from the loaded list.
file_path = search_for_file(const.underscore)
if file_path
expanded = File.expand_path(file_path)
expanded.delete_suffix!(".rb")
loaded.delete(expanded)
end
if constants.empty?
parent = Object
else
# This method is robust to non-reachable constants.
#
# Non-reachable constants may be passed if some of the parents were
# autoloaded and already removed. It is easier to do a sanity check
# here than require the caller to be clever. We check the parent
# rather than the very const argument because we do not want to
# trigger Kernel#autoloads, see the comment below.
parent_name = constants.join("::")
return unless qualified_const_defined?(parent_name)
parent = constantize(parent_name)
end
# In an autoloaded user.rb like this
#
# autoload :Foo, 'foo'
#
# class User < ActiveRecord::Base
# end
#
# we correctly register "Foo" as being autoloaded. But if the app does
# not use the "Foo" constant we need to be careful not to trigger
# loading "foo.rb" ourselves. While #const_defined? and #const_get? do
# require the file, #autoload? and #remove_const don't.
#
# We are going to remove the constant nonetheless ---which exists as
# far as Ruby is concerned--- because if the user removes the macro
# call from a class or module that were not autoloaded, as in the
# example above with Object, accessing to that constant must err.
unless parent.autoload?(to_remove)
begin
constantized = parent.const_get(to_remove, false)
rescue NameError
# The constant is no longer reachable, just skip it.
return
else
constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
end
end
begin
parent.instance_eval { remove_const to_remove }
rescue NameError
# The constant is no longer reachable, just skip it.
end
end
- 3
def log(message)
logger.debug("autoloading: #{message}") if logger && verbose
end
- 3
private
- 3
if RUBY_VERSION < "2.6"
- 3
def uninitialized_constant(qualified_name, const_name, receiver:)
NameError.new("uninitialized constant #{qualified_name}", const_name)
end
else
def uninitialized_constant(qualified_name, const_name, receiver:)
NameError.new("uninitialized constant #{qualified_name}", const_name, receiver: receiver)
end
end
# Returns the original name of a class or module even if `name` has been
# overridden.
- 3
def real_mod_name(mod)
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
end
end
end
- 3
ActiveSupport::Dependencies.hook!
# frozen_string_literal: true
- 24
require "active_support/inflector/methods"
- 24
module ActiveSupport
# Autoload and eager load conveniences for your library.
#
# This module allows you to define autoloads based on
# Rails conventions (i.e. no need to define the path
# it is automatically guessed based on the filename)
# and also define a set of constants that needs to be
# eager loaded:
#
# module MyLib
# extend ActiveSupport::Autoload
#
# autoload :Model
#
# eager_autoload do
# autoload :Cache
# end
# end
#
# Then your library can be eager loaded by simply calling:
#
# MyLib.eager_load!
- 24
module Autoload
- 24
def self.extended(base) # :nodoc:
- 27
base.class_eval do
- 27
@_autoloads = {}
- 27
@_under_path = nil
- 27
@_at_path = nil
- 27
@_eager_autoload = false
end
end
- 24
def autoload(const_name, path = @_at_path)
- 1023
unless path
- 993
full = [name, @_under_path, const_name.to_s].compact.join("::")
- 993
path = Inflector.underscore(full)
end
- 1023
if @_eager_autoload
- 585
@_autoloads[const_name] = path
end
- 1023
super const_name, path
end
- 24
def autoload_under(path)
@_under_path, old_path = path, @_under_path
yield
ensure
@_under_path = old_path
end
- 24
def autoload_at(path)
@_at_path, old_path = path, @_at_path
yield
ensure
@_at_path = old_path
end
- 24
def eager_autoload
- 25
old_eager, @_eager_autoload = @_eager_autoload, true
- 25
yield
ensure
- 25
@_eager_autoload = old_eager
end
- 24
def eager_load!
@_autoloads.each_value { |file| require file }
end
- 24
def autoloads
@_autoloads
end
end
end
# frozen_string_literal: true
- 3
require "active_support/concurrency/share_lock"
- 3
module ActiveSupport #:nodoc:
- 3
module Dependencies #:nodoc:
- 3
class Interlock
- 3
def initialize # :nodoc:
- 3
@lock = ActiveSupport::Concurrency::ShareLock.new
end
- 3
def loading
@lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do
yield
end
end
- 3
def unloading
@lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do
yield
end
end
- 3
def start_unloading
@lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
end
- 3
def done_unloading
@lock.stop_exclusive(compatible: [:load, :unload])
end
- 3
def start_running
@lock.start_sharing
end
- 3
def done_running
@lock.stop_sharing
end
- 3
def running
@lock.sharing do
yield
end
end
- 3
def permit_concurrent_loads
@lock.yield_shares(compatible: [:load]) do
yield
end
end
- 3
def raw_state(&block) # :nodoc:
@lock.raw_state(&block)
end
end
end
end
# frozen_string_literal: true
- 1
require "set"
- 1
require "active_support/core_ext/string/inflections"
- 1
module ActiveSupport
- 1
module Dependencies
- 1
module ZeitwerkIntegration # :nodoc: all
- 1
module Decorations
- 1
def clear
Dependencies.unload_interlock do
Rails.autoloaders.main.reload
rescue Zeitwerk::ReloadingDisabledError
raise "reloading is disabled because config.cache_classes is true"
end
end
- 1
def constantize(cpath)
ActiveSupport::Inflector.constantize(cpath)
end
- 1
def safe_constantize(cpath)
ActiveSupport::Inflector.safe_constantize(cpath)
end
- 1
def autoloaded_constants
Rails.autoloaders.main.unloadable_cpaths
end
- 1
def autoloaded?(object)
cpath = object.is_a?(Module) ? real_mod_name(object) : object.to_s
Rails.autoloaders.main.unloadable_cpath?(cpath)
end
- 1
def verbose=(verbose)
l = verbose ? logger || Rails.logger : nil
Rails.autoloaders.each { |autoloader| autoloader.logger = l }
end
- 1
def unhook!
:no_op
end
end
- 1
module RequireDependency
- 1
def require_dependency(filename)
filename = filename.to_path if filename.respond_to?(:to_path)
if abspath = ActiveSupport::Dependencies.search_for_file(filename)
require abspath
else
require filename
end
end
end
- 1
module Inflector
# Concurrent::Map is not needed. This is a private class, and overrides
# must be defined while the application boots.
- 1
@overrides = {}
- 1
def self.camelize(basename, _abspath)
@overrides[basename] || basename.camelize
end
- 1
def self.inflect(overrides)
@overrides.merge!(overrides)
end
end
- 1
class << self
- 1
def take_over(enable_reloading:)
setup_autoloaders(enable_reloading)
freeze_paths
decorate_dependencies
end
- 1
private
- 1
def setup_autoloaders(enable_reloading)
Dependencies.autoload_paths.each do |autoload_path|
# Zeitwerk only accepts existing directories in `push_dir` to
# prevent misconfigurations.
next unless File.directory?(autoload_path)
autoloader = \
autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main
autoloader.push_dir(autoload_path)
autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
end
Rails.autoloaders.main.enable_reloading if enable_reloading
Rails.autoloaders.each(&:setup)
end
- 1
def autoload_once?(autoload_path)
Dependencies.autoload_once_paths.include?(autoload_path)
end
- 1
def eager_load?(autoload_path)
Dependencies._eager_load_paths.member?(autoload_path)
end
- 1
def freeze_paths
Dependencies.autoload_paths.freeze
Dependencies.autoload_once_paths.freeze
Dependencies._eager_load_paths.freeze
end
- 1
def decorate_dependencies
Dependencies.unhook!
Dependencies.singleton_class.prepend(Decorations)
Object.prepend(RequireDependency)
end
end
end
end
end
# frozen_string_literal: true
- 24
require "singleton"
- 24
module ActiveSupport
# \Deprecation specifies the API used by Rails to deprecate methods, instance
# variables, objects and constants.
- 24
class Deprecation
# active_support.rb sets an autoload for ActiveSupport::Deprecation.
#
# If these requires were at the top of the file the constant would not be
# defined by the time their files were loaded. Since some of them reopen
# ActiveSupport::Deprecation its autoload would be triggered, resulting in
# a circular require warning for active_support/deprecation.rb.
#
# So, we define the constant first, and load dependencies later.
- 24
require "active_support/deprecation/instance_delegator"
- 24
require "active_support/deprecation/behaviors"
- 24
require "active_support/deprecation/reporting"
- 24
require "active_support/deprecation/disallowed"
- 24
require "active_support/deprecation/constant_accessor"
- 24
require "active_support/deprecation/method_wrappers"
- 23
require "active_support/deprecation/proxy_wrappers"
- 23
require "active_support/core_ext/module/deprecation"
- 23
require "concurrent/atomic/thread_local_var"
- 23
include Singleton
- 23
include InstanceDelegator
- 23
include Behavior
- 23
include Reporting
- 23
include Disallowed
- 23
include MethodWrapper
# The version number in which the deprecated behavior will be removed, by default.
- 23
attr_accessor :deprecation_horizon
# It accepts two parameters on initialization. The first is a version of library
# and the second is a library name.
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- 23
def initialize(deprecation_horizon = "6.2", gem_name = "Rails")
- 23
self.gem_name = gem_name
- 23
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
- 23
self.silenced = false
- 23
self.debug = false
- 23
@silenced_thread = Concurrent::ThreadLocalVar.new(false)
- 23
@explicitly_allowed_warnings = Concurrent::ThreadLocalVar.new(nil)
end
end
end
# frozen_string_literal: true
- 24
require "active_support/notifications"
- 24
module ActiveSupport
# Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>.
# You would set <tt>:raise</tt>, as a behavior to raise errors and proactively report exceptions from deprecations.
- 24
class DeprecationException < StandardError
end
- 24
class Deprecation
# Default warning behaviors per Rails.env.
- 24
DEFAULT_BEHAVIORS = {
raise: ->(message, callstack, deprecation_horizon, gem_name) {
e = DeprecationException.new(message)
e.set_backtrace(callstack.map(&:to_s))
raise e
},
stderr: ->(message, callstack, deprecation_horizon, gem_name) {
$stderr.puts(message)
$stderr.puts callstack.join("\n ") if debug
},
log: ->(message, callstack, deprecation_horizon, gem_name) {
logger =
if defined?(Rails.logger) && Rails.logger
Rails.logger
else
require "active_support/logger"
ActiveSupport::Logger.new($stderr)
end
logger.warn message
logger.debug callstack.join("\n ") if debug
},
notify: ->(message, callstack, deprecation_horizon, gem_name) {
notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}"
ActiveSupport::Notifications.instrument(notification_name,
message: message,
callstack: callstack,
gem_name: gem_name,
deprecation_horizon: deprecation_horizon)
},
silence: ->(message, callstack, deprecation_horizon, gem_name) { },
}
# Behavior module allows to determine how to display deprecation messages.
# You can create a custom behavior or set any from the +DEFAULT_BEHAVIORS+
# constant. Available behaviors are:
#
# [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
# [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
# [+log+] Log all deprecation warnings to +Rails.logger+.
# [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
# [+silence+] Do nothing.
#
# Setting behaviors only affects deprecations that happen after boot time.
# For more information you can read the documentation of the +behavior=+ method.
- 24
module Behavior
# Whether to print a backtrace along with the warning.
- 24
attr_accessor :debug
# Returns the current behavior or if one isn't set, defaults to +:stderr+.
- 24
def behavior
@behavior ||= [DEFAULT_BEHAVIORS[:stderr]]
end
# Returns the current behavior for disallowed deprecations or if one isn't set, defaults to +:raise+.
- 24
def disallowed_behavior
@disallowed_behavior ||= [DEFAULT_BEHAVIORS[:raise]]
end
# Sets the behavior to the specified value. Can be a single value, array,
# or an object that responds to +call+.
#
# Available behaviors:
#
# [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
# [+stderr+] Log all deprecation warnings to <tt>$stderr</tt>.
# [+log+] Log all deprecation warnings to +Rails.logger+.
# [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
# [+silence+] Do nothing.
#
# Setting behaviors only affects deprecations that happen after boot time.
# Deprecation warnings raised by gems are not affected by this setting
# because they happen before Rails boots up.
#
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
# ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# # custom stuff
# }
- 24
def behavior=(behavior)
@behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
end
# Sets the behavior for disallowed deprecations (those configured by
# ActiveSupport::Deprecation.disallowed_warnings=) to the specified
# value. As with +behavior=+, this can be a single value, array, or an
# object that responds to +call+.
- 24
def disallowed_behavior=(behavior)
@disallowed_behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) }
end
- 24
private
- 24
def arity_coerce(behavior)
unless behavior.respond_to?(:call)
raise ArgumentError, "#{behavior.inspect} is not a valid deprecation behavior."
end
if behavior.arity == 4 || behavior.arity == -1
behavior
else
-> message, callstack, _, _ { behavior.call(message, callstack) }
end
end
end
end
end
# frozen_string_literal: true
- 24
module ActiveSupport
- 24
class Deprecation
# DeprecatedConstantAccessor transforms a constant into a deprecated one by
# hooking +const_missing+.
#
# It takes the names of an old (deprecated) constant and of a new constant
# (both in string form) and optionally a deprecator. The deprecator defaults
# to +ActiveSupport::Deprecator+ if none is specified.
#
# The deprecated constant now returns the same object as the new one rather
# than a proxy object, so it can be used transparently in +rescue+ blocks
# etc.
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
# # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# include ActiveSupport::Deprecation::DeprecatedConstantAccessor
# deprecate_constant 'PLANETS', 'PLANETS_POST_2006'
#
# PLANETS.map { |planet| planet.capitalize }
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
# (Backtrace information…)
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
- 24
module DeprecatedConstantAccessor
- 24
def self.included(base)
- 1
require "active_support/inflector/methods"
- 1
extension = Module.new do
- 1
def const_missing(missing_const_name)
if class_variable_defined?(:@@_deprecated_constants)
if (replacement = class_variable_get(:@@_deprecated_constants)[missing_const_name.to_s])
replacement[:deprecator].warn(replacement[:message] || "#{name}::#{missing_const_name} is deprecated! Use #{replacement[:new]} instead.", caller_locations)
return ActiveSupport::Inflector.constantize(replacement[:new].to_s)
end
end
super
end
- 1
def deprecate_constant(const_name, new_constant, message: nil, deprecator: ActiveSupport::Deprecation.instance)
- 2
class_variable_set(:@@_deprecated_constants, {}) unless class_variable_defined?(:@@_deprecated_constants)
- 2
class_variable_get(:@@_deprecated_constants)[const_name.to_s] = { new: new_constant, message: message, deprecator: deprecator }
end
end
- 1
base.singleton_class.prepend extension
end
end
end
end
# frozen_string_literal: true
- 24
module ActiveSupport
- 24
class Deprecation
- 24
module Disallowed
# Sets the criteria used to identify deprecation messages which should be
# disallowed. Can be an array containing strings, symbols, or regular
# expressions. (Symbols are treated as strings). These are compared against
# the text of the generated deprecation warning.
#
# Additionally the scalar symbol +:all+ may be used to treat all
# deprecations as disallowed.
#
# Deprecations matching a substring or regular expression will be handled
# using the configured +ActiveSupport::Deprecation.disallowed_behavior+
# rather than +ActiveSupport::Deprecation.behavior+
- 24
attr_writer :disallowed_warnings
# Returns the configured criteria used to identify deprecation messages
# which should be treated as disallowed.
- 24
def disallowed_warnings
@disallowed_warnings ||= []
end
- 24
private
- 24
def deprecation_disallowed?(message)
disallowed = ActiveSupport::Deprecation.disallowed_warnings
return false if explicitly_allowed?(message)
return true if disallowed == :all
disallowed.any? do |rule|
case rule
when String, Symbol
message.include?(rule.to_s)
when Regexp
rule.match?(message)
end
end
end
- 24
def explicitly_allowed?(message)
allowances = @explicitly_allowed_warnings.value
return false unless allowances
return true if allowances == :all
allowances = [allowances] unless allowances.kind_of?(Array)
allowances.any? do |rule|
case rule
when String, Symbol
message.include?(rule.to_s)
when Regexp
rule.match?(message)
end
end
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/core_ext/module/delegation"
- 24
module ActiveSupport
- 24
class Deprecation
- 24
module InstanceDelegator # :nodoc:
- 24
def self.included(base)
- 23
base.extend(ClassMethods)
- 23
base.singleton_class.prepend(OverrideDelegators)
- 23
base.public_class_method :new
end
- 24
module ClassMethods # :nodoc:
- 24
def include(included_module)
- 483
included_module.instance_methods.each { |m| method_added(m) }
- 92
super
end
- 24
def method_added(method_name)
- 460
singleton_class.delegate(method_name, to: :instance)
end
end
- 24
module OverrideDelegators # :nodoc:
- 24
def warn(message = nil, callstack = nil)
callstack ||= caller_locations(2)
super
end
- 24
def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
caller_backtrace ||= caller_locations(2)
super
end
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/core_ext/array/extract_options"
- 24
require "active_support/core_ext/module/redefine_method"
- 23
module ActiveSupport
- 23
class Deprecation
- 23
module MethodWrapper
# Declare that a method has been deprecated.
#
# class Fred
# def aaa; end
# def bbb; end
# def ccc; end
# def ddd; end
# def eee; end
# end
#
# Using the default deprecator:
# ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
# # => Fred
#
# Fred.new.aaa
# # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
# # => nil
#
# Fred.new.bbb
# # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
# # => nil
#
# Fred.new.ccc
# # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
# # => nil
#
# Passing in a custom deprecator:
# custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
# ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
# # => [:ddd]
#
# Fred.new.ddd
# DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
# # => nil
#
# Using a custom deprecator directly:
# custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem')
# custom_deprecator.deprecate_methods(Fred, eee: :zzz)
# # => [:eee]
#
# Fred.new.eee
# DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
# # => nil
- 23
def deprecate_methods(target_module, *method_names)
- 4
options = method_names.extract_options!
- 4
deprecator = options.delete(:deprecator) || self
- 4
method_names += options.keys
- 4
mod = nil
- 4
method_names.each do |method_name|
- 9
message = options[method_name]
- 9
if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
- 8
method = target_module.instance_method(method_name)
- 8
target_module.module_eval do
- 8
redefine_method(method_name) do |*args, &block|
deprecator.deprecation_warning(method_name, message)
method.bind(self).call(*args, &block)
end
- 8
ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
end
else
- 1
mod ||= Module.new
- 1
mod.module_eval do
- 1
define_method(method_name) do |*args, &block|
deprecator.deprecation_warning(method_name, message)
super(*args, &block)
end
- 1
ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
end
end
end
- 4
target_module.prepend(mod) if mod
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport
- 23
class Deprecation
- 23
class DeprecationProxy #:nodoc:
- 23
def self.new(*args, &block)
object = args.first
return object unless object
super
end
- 1541
instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
# Don't give a deprecation warning on inspect since test/unit and error
# logs rely on it for diagnostics.
- 23
def inspect
target.inspect
end
- 23
private
- 23
def method_missing(called, *args, &block)
warn caller_locations, called, args
target.__send__(called, *args, &block)
end
end
# DeprecatedObjectProxy transforms an object into a deprecated one. It
# takes an object, a deprecation message and optionally a deprecator. The
# deprecator defaults to +ActiveSupport::Deprecator+ if none is specified.
#
# deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated")
# # => #<Object:0x007fb9b34c34b0>
#
# deprecated_object.to_s
# DEPRECATION WARNING: This object is now deprecated.
# (Backtrace)
# # => "#<Object:0x007fb9b34c34b0>"
- 23
class DeprecatedObjectProxy < DeprecationProxy
- 23
def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance)
@object = object
@message = message
@deprecator = deprecator
end
- 23
private
- 23
def target
@object
end
- 23
def warn(callstack, called, args)
@deprecator.warn(@message, callstack)
end
end
# DeprecatedInstanceVariableProxy transforms an instance variable into a
# deprecated one. It takes an instance of a class, a method on that class
# and an instance variable. It optionally takes a deprecator as the last
# argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none
# is specified.
#
# class Example
# def initialize
# @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request)
# @_request = :special_request
# end
#
# def request
# @_request
# end
#
# def old_request
# @request
# end
# end
#
# example = Example.new
# # => #<Example:0x007fb9b31090b8 @_request=:special_request, @request=:special_request>
#
# example.old_request.to_s
# # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of
# @request.to_s
# (Backtrace information…)
# "special_request"
#
# example.request.to_s
# # => "special_request"
- 23
class DeprecatedInstanceVariableProxy < DeprecationProxy
- 23
def initialize(instance, method, var = "@#{method}", deprecator = ActiveSupport::Deprecation.instance)
@instance = instance
@method = method
@var = var
@deprecator = deprecator
end
- 23
private
- 23
def target
@instance.__send__(@method)
end
- 23
def warn(callstack, called, args)
@deprecator.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack)
end
end
# DeprecatedConstantProxy transforms a constant into a deprecated one. It
# takes the names of an old (deprecated) constant and of a new constant
# (both in string form) and optionally a deprecator. The deprecator defaults
# to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant
# now returns the value of the new one.
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
# # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
#
# PLANETS.map { |planet| planet.capitalize }
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
# (Backtrace information…)
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
- 23
class DeprecatedConstantProxy < Module
- 23
def self.new(*args, **options, &block)
- 2
object = args.first
- 2
return object unless object
- 2
super
end
- 23
def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
- 2
Kernel.require "active_support/inflector/methods"
- 2
@old_const = old_const
- 2
@new_const = new_const
- 2
@deprecator = deprecator
- 2
@message = message
end
- 2970
instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
# Don't give a deprecation warning on inspect since test/unit and error
# logs rely on it for diagnostics.
- 23
def inspect
target.inspect
end
# Don't give a deprecation warning on methods that IRB may invoke
# during tab-completion.
- 23
delegate :hash, :instance_methods, :name, :respond_to?, to: :target
# Returns the class of the new constant.
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
# PLANETS.class # => Array
- 23
def class
target.class
end
- 23
private
- 23
def target
ActiveSupport::Inflector.constantize(@new_const.to_s)
end
- 23
def const_missing(name)
@deprecator.warn(@message, caller_locations)
target.const_get(name)
end
- 23
def method_missing(called, *args, &block)
@deprecator.warn(@message, caller_locations)
target.__send__(called, *args, &block)
end
end
end
end
# frozen_string_literal: true
- 24
require "rbconfig"
- 24
module ActiveSupport
- 24
class Deprecation
- 24
module Reporting
# Whether to print a message (silent mode)
- 24
attr_writer :silenced
# Name of gem where method is deprecated
- 24
attr_accessor :gem_name
# Outputs a deprecation warning to the output configured by
# <tt>ActiveSupport::Deprecation.behavior</tt>.
#
# ActiveSupport::Deprecation.warn('something broke!')
# # => "DEPRECATION WARNING: something broke! (called from your_code.rb:1)"
- 24
def warn(message = nil, callstack = nil)
return if silenced
callstack ||= caller_locations(2)
deprecation_message(callstack, message).tap do |m|
if deprecation_disallowed?(message)
disallowed_behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
else
behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) }
end
end
end
# Silence deprecation warnings within the block.
#
# ActiveSupport::Deprecation.warn('something broke!')
# # => "DEPRECATION WARNING: something broke! (called from your_code.rb:1)"
#
# ActiveSupport::Deprecation.silence do
# ActiveSupport::Deprecation.warn('something broke!')
# end
# # => nil
- 24
def silence(&block)
@silenced_thread.bind(true, &block)
end
# Allow previously disallowed deprecation warnings within the block.
# <tt>allowed_warnings</tt> can be an array containing strings, symbols, or regular
# expressions. (Symbols are treated as strings). These are compared against
# the text of deprecation warning messages generated within the block.
# Matching warnings will be exempt from the rules set by
# +ActiveSupport::Deprecation.disallowed_warnings+
#
# The optional <tt>if:</tt> argument accepts a truthy/falsy value or an object that
# responds to <tt>.call</tt>. If truthy, then matching warnings will be allowed.
# If falsey then the method yields to the block without allowing the warning.
#
# ActiveSupport::Deprecation.disallowed_behavior = :raise
# ActiveSupport::Deprecation.disallowed_warnings = [
# "something broke"
# ]
#
# ActiveSupport::Deprecation.warn('something broke!')
# # => ActiveSupport::DeprecationException
#
# ActiveSupport::Deprecation.allow ['something broke'] do
# ActiveSupport::Deprecation.warn('something broke!')
# end
# # => nil
#
# ActiveSupport::Deprecation.allow ['something broke'], if: Rails.env.production? do
# ActiveSupport::Deprecation.warn('something broke!')
# end
# # => ActiveSupport::DeprecationException for dev/test, nil for production
- 24
def allow(allowed_warnings = :all, if: true, &block)
conditional = binding.local_variable_get(:if)
conditional = conditional.call if conditional.respond_to?(:call)
if conditional
@explicitly_allowed_warnings.bind(allowed_warnings, &block)
else
yield
end
end
- 24
def silenced
@silenced || @silenced_thread.value
end
- 24
def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
caller_backtrace ||= caller_locations(2)
deprecated_method_warning(deprecated_method_name, message).tap do |msg|
warn(msg, caller_backtrace)
end
end
- 24
private
# Outputs a deprecation warning message
#
# deprecated_method_warning(:method_name)
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}"
# deprecated_method_warning(:method_name, :another_method)
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)"
# deprecated_method_warning(:method_name, "Optional message")
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)"
- 24
def deprecated_method_warning(method_name, message = nil)
warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}"
case message
when Symbol then "#{warning} (use #{message} instead)"
when String then "#{warning} (#{message})"
else warning
end
end
- 24
def deprecation_message(callstack, message = nil)
message ||= "You are using deprecated behavior which will be removed from the next major or minor release."
"DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}"
end
- 24
def deprecation_caller_message(callstack)
file, line, method = extract_callstack(callstack)
if file
if line && method
"(called from #{method} at #{file}:#{line})"
else
"(called from #{file}:#{line})"
end
end
end
- 24
def extract_callstack(callstack)
return _extract_callstack(callstack) if callstack.first.is_a? String
offending_line = callstack.find { |frame|
frame.absolute_path && !ignored_callstack(frame.absolute_path)
} || callstack.first
[offending_line.path, offending_line.lineno, offending_line.label]
end
- 24
def _extract_callstack(callstack)
warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first
if offending_line
if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
md.captures
else
offending_line
end
end
end
- 24
RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
- 24
def ignored_callstack(path)
path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
end
end
end
end
# frozen_string_literal: true
- 23
require "weakref"
- 23
module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
- 23
module DescendantsTracker
- 23
@@direct_descendants = {}
- 23
class << self
- 23
def direct_descendants(klass)
descendants = @@direct_descendants[klass]
descendants ? descendants.to_a : []
end
- 23
alias_method :subclasses, :direct_descendants
- 23
def descendants(klass)
- 211
arr = []
- 211
accumulate_descendants(klass, arr)
- 211
arr
end
- 23
def clear
if defined? ActiveSupport::Dependencies
@@direct_descendants.each do |klass, descendants|
if Dependencies.autoloaded?(klass)
@@direct_descendants.delete(klass)
else
descendants.reject! { |v| Dependencies.autoloaded?(v) }
end
end
else
@@direct_descendants.clear
end
end
# This is the only method that is not thread safe, but is only ever called
# during the eager loading phase.
- 23
def store_inherited(klass, descendant)
- 334
(@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
end
- 23
private
- 23
def accumulate_descendants(klass, acc)
- 211
if direct_descendants = @@direct_descendants[klass]
direct_descendants.each do |direct_descendant|
acc << direct_descendant
accumulate_descendants(direct_descendant, acc)
end
end
end
end
- 23
def inherited(base)
- 334
DescendantsTracker.store_inherited(self, base)
- 334
super
end
- 23
def direct_descendants
DescendantsTracker.direct_descendants(self)
end
- 23
alias_method :subclasses, :direct_descendants
- 23
def descendants
DescendantsTracker.descendants(self)
end
# DescendantsArray is an array that contains weak references to classes.
- 23
class DescendantsArray # :nodoc:
- 23
include Enumerable
- 23
def initialize
- 54
@refs = []
end
- 23
def initialize_copy(orig)
@refs = @refs.dup
end
- 23
def <<(klass)
- 334
@refs << WeakRef.new(klass)
end
- 23
def each
@refs.reject! do |ref|
yield ref.__getobj__
false
rescue WeakRef::RefError
true
end
self
end
- 23
def refs_size
@refs.size
end
- 23
def cleanup!
@refs.delete_if { |ref| !ref.weakref_alive? }
end
- 23
def reject!
@refs.reject! do |ref|
yield ref.__getobj__
rescue WeakRef::RefError
true
end
end
end
end
end
# frozen_string_literal: true
module ActiveSupport
class Digest #:nodoc:
class <<self
def hash_digest_class
@hash_digest_class ||= ::Digest::MD5
end
def hash_digest_class=(klass)
raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest)
@hash_digest_class = klass
end
def hexdigest(arg)
hash_digest_class.hexdigest(arg)[0...32]
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/array/conversions"
- 23
require "active_support/core_ext/module/delegation"
- 23
require "active_support/core_ext/object/acts_like"
- 23
require "active_support/core_ext/string/filters"
- 23
module ActiveSupport
# Provides accurate date and time measurements using Date#advance and
# Time#advance, respectively. It mainly supports the methods on Numeric.
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
- 23
class Duration
- 23
class Scalar < Numeric #:nodoc:
- 23
attr_reader :value
- 23
delegate :to_i, :to_f, :to_s, to: :value
- 23
def initialize(value)
@value = value
end
- 23
def coerce(other)
[Scalar.new(other), self]
end
- 23
def -@
Scalar.new(-value)
end
- 23
def <=>(other)
if Scalar === other || Duration === other
value <=> other.value
elsif Numeric === other
value <=> other
else
nil
end
end
- 23
def +(other)
if Duration === other
seconds = value + other.parts.fetch(:seconds, 0)
new_parts = other.parts.merge(seconds: seconds)
new_value = value + other.value
Duration.new(new_value, new_parts)
else
calculate(:+, other)
end
end
- 23
def -(other)
if Duration === other
seconds = value - other.parts.fetch(:seconds, 0)
new_parts = other.parts.transform_values(&:-@)
new_parts = new_parts.merge(seconds: seconds)
new_value = value - other.value
Duration.new(new_value, new_parts)
else
calculate(:-, other)
end
end
- 23
def *(other)
if Duration === other
new_parts = other.parts.transform_values { |other_value| value * other_value }
new_value = value * other.value
Duration.new(new_value, new_parts)
else
calculate(:*, other)
end
end
- 23
def /(other)
if Duration === other
value / other.value
else
calculate(:/, other)
end
end
- 23
def %(other)
if Duration === other
Duration.build(value % other.value)
else
calculate(:%, other)
end
end
- 23
private
- 23
def calculate(op, other)
if Scalar === other
Scalar.new(value.public_send(op, other.value))
elsif Numeric === other
Scalar.new(value.public_send(op, other))
else
raise_type_error(other)
end
end
- 23
def raise_type_error(other)
raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
end
end
- 23
SECONDS_PER_MINUTE = 60
- 23
SECONDS_PER_HOUR = 3600
- 23
SECONDS_PER_DAY = 86400
- 23
SECONDS_PER_WEEK = 604800
- 23
SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year
- 23
SECONDS_PER_YEAR = 31556952 # length of a gregorian year (365.2425 days)
- 23
PARTS_IN_SECONDS = {
seconds: 1,
minutes: SECONDS_PER_MINUTE,
hours: SECONDS_PER_HOUR,
days: SECONDS_PER_DAY,
weeks: SECONDS_PER_WEEK,
months: SECONDS_PER_MONTH,
years: SECONDS_PER_YEAR
}.freeze
- 23
PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze
- 23
attr_accessor :value, :parts
- 23
autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
- 23
autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer"
- 23
class << self
# Creates a new Duration from string formatted according to ISO 8601 Duration.
#
# See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
# This method allows negative parts to be present in pattern.
# If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
- 23
def parse(iso8601duration)
parts = ISO8601Parser.new(iso8601duration).parse!
new(calculate_total_seconds(parts), parts)
end
- 23
def ===(other) #:nodoc:
other.is_a?(Duration)
rescue ::NoMethodError
false
end
- 23
def seconds(value) #:nodoc:
new(value, seconds: value)
end
- 23
def minutes(value) #:nodoc:
new(value * SECONDS_PER_MINUTE, minutes: value)
end
- 23
def hours(value) #:nodoc:
new(value * SECONDS_PER_HOUR, hours: value)
end
- 23
def days(value) #:nodoc:
new(value * SECONDS_PER_DAY, days: value)
end
- 23
def weeks(value) #:nodoc:
new(value * SECONDS_PER_WEEK, weeks: value)
end
- 23
def months(value) #:nodoc:
new(value * SECONDS_PER_MONTH, months: value)
end
- 23
def years(value) #:nodoc:
new(value * SECONDS_PER_YEAR, years: value)
end
# Creates a new Duration from a seconds value that is converted
# to the individual parts:
#
# ActiveSupport::Duration.build(31556952).parts # => {:years=>1}
# ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
#
- 23
def build(value)
unless value.is_a?(::Numeric)
raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
end
parts = {}
remainder = value.round(9)
PARTS.each do |part|
unless part == :seconds
part_in_seconds = PARTS_IN_SECONDS[part]
parts[part] = remainder.div(part_in_seconds)
remainder %= part_in_seconds
end
end unless value == 0
parts[:seconds] = remainder
new(value, parts)
end
- 23
private
- 23
def calculate_total_seconds(parts)
parts.inject(0) do |total, (part, value)|
total + value * PARTS_IN_SECONDS[part]
end
end
end
- 23
def initialize(value, parts) #:nodoc:
@value, @parts = value, parts
@parts.reject! { |k, v| v.zero? } unless value == 0
end
- 23
def coerce(other) #:nodoc:
case other
when Scalar
[other, self]
when Duration
[Scalar.new(other.value), self]
else
[Scalar.new(other), self]
end
end
# Compares one Duration with another or a Numeric to this Duration.
# Numeric values are treated as seconds.
- 23
def <=>(other)
if Duration === other
value <=> other.value
elsif Numeric === other
value <=> other
end
end
# Adds another Duration or a Numeric to this Duration. Numeric values
# are treated as seconds.
- 23
def +(other)
if Duration === other
parts = @parts.merge(other.parts) do |_key, value, other_value|
value + other_value
end
Duration.new(value + other.value, parts)
else
seconds = @parts.fetch(:seconds, 0) + other
Duration.new(value + other, @parts.merge(seconds: seconds))
end
end
# Subtracts another Duration or a Numeric from this Duration. Numeric
# values are treated as seconds.
- 23
def -(other)
self + (-other)
end
# Multiplies this Duration by a Numeric and returns a new Duration.
- 23
def *(other)
if Scalar === other || Duration === other
Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
elsif Numeric === other
Duration.new(value * other, parts.transform_values { |number| number * other })
else
raise_type_error(other)
end
end
# Divides this Duration by a Numeric and returns a new Duration.
- 23
def /(other)
if Scalar === other
Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
elsif Duration === other
value / other.value
elsif Numeric === other
Duration.new(value / other, parts.transform_values { |number| number / other })
else
raise_type_error(other)
end
end
# Returns the modulo of this Duration by another Duration or Numeric.
# Numeric values are treated as seconds.
- 23
def %(other)
if Duration === other || Scalar === other
Duration.build(value % other.value)
elsif Numeric === other
Duration.build(value % other)
else
raise_type_error(other)
end
end
- 23
def -@ #:nodoc:
Duration.new(-value, parts.transform_values(&:-@))
end
- 23
def +@ #:nodoc:
self
end
- 23
def is_a?(klass) #:nodoc:
Duration == klass || value.is_a?(klass)
end
- 23
alias :kind_of? :is_a?
- 23
def instance_of?(klass) # :nodoc:
Duration == klass || value.instance_of?(klass)
end
# Returns +true+ if +other+ is also a Duration instance with the
# same +value+, or if <tt>other == value</tt>.
- 23
def ==(other)
if Duration === other
other.value == value
else
other == value
end
end
# Returns the amount of seconds a duration covers as a string.
# For more information check to_i method.
#
# 1.day.to_s # => "86400"
- 23
def to_s
@value.to_s
end
# Returns the number of seconds that this Duration represents.
#
# 1.minute.to_i # => 60
# 1.hour.to_i # => 3600
# 1.day.to_i # => 86400
#
# Note that this conversion makes some assumptions about the
# duration of some periods, e.g. months are always 1/12 of year
# and years are 365.2425 days:
#
# # equivalent to (1.year / 12).to_i
# 1.month.to_i # => 2629746
#
# # equivalent to 365.2425.days.to_i
# 1.year.to_i # => 31556952
#
# In such cases, Ruby's core
# Date[https://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
# Time[https://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
# date and time arithmetic.
- 23
def to_i
@value.to_i
end
# Returns +true+ if +other+ is also a Duration instance, which has the
# same parts as this one.
- 23
def eql?(other)
Duration === other && other.value.eql?(value)
end
- 23
def hash
@value.hash
end
# Calculates a new Time or Date that is as far in the future
# as this Duration represents.
- 23
def since(time = ::Time.current)
sum(1, time)
end
- 23
alias :from_now :since
- 23
alias :after :since
# Calculates a new Time or Date that is as far in the past
# as this Duration represents.
- 23
def ago(time = ::Time.current)
sum(-1, time)
end
- 23
alias :until :ago
- 23
alias :before :ago
- 23
def inspect #:nodoc:
return "#{value} seconds" if parts.empty?
parts.
sort_by { |unit, _ | PARTS.index(unit) }.
map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
to_sentence(locale: ::I18n.default_locale)
end
- 23
def as_json(options = nil) #:nodoc:
to_i
end
- 23
def init_with(coder) #:nodoc:
initialize(coder["value"], coder["parts"])
end
- 23
def encode_with(coder) #:nodoc:
coder.map = { "value" => @value, "parts" => @parts }
end
# Build ISO 8601 Duration string for this duration.
# The +precision+ parameter can be used to limit seconds' precision of duration.
- 23
def iso8601(precision: nil)
ISO8601Serializer.new(self, precision: precision).serialize
end
- 23
private
- 23
def sum(sign, time = ::Time.current)
unless time.acts_like?(:time) || time.acts_like?(:date)
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
if parts.empty?
time.since(sign * value)
else
parts.inject(time) do |t, (type, number)|
if type == :seconds
t.since(sign * number)
elsif type == :minutes
t.since(sign * number * 60)
elsif type == :hours
t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
end
end
end
- 23
def respond_to_missing?(method, _)
value.respond_to?(method)
end
- 23
def method_missing(method, *args, &block)
value.public_send(method, *args, &block)
end
- 23
def raise_type_error(other)
raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
end
end
end
# frozen_string_literal: true
require "strscan"
module ActiveSupport
class Duration
# Parses a string formatted according to ISO 8601 Duration into the hash.
#
# See {ISO 8601}[https://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
#
# This parser allows negative parts to be present in pattern.
class ISO8601Parser # :nodoc:
class ParsingError < ::ArgumentError; end
PERIOD_OR_COMMA = /\.|,/
PERIOD = "."
COMMA = ","
SIGN_MARKER = /\A\-|\+|/
DATE_MARKER = /P/
TIME_MARKER = /T/
DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
DATE_TO_PART = { "Y" => :years, "M" => :months, "W" => :weeks, "D" => :days }
TIME_TO_PART = { "H" => :hours, "M" => :minutes, "S" => :seconds }
DATE_COMPONENTS = [:years, :months, :days]
TIME_COMPONENTS = [:hours, :minutes, :seconds]
attr_reader :parts, :scanner
attr_accessor :mode, :sign
def initialize(string)
@scanner = StringScanner.new(string)
@parts = {}
@mode = :start
@sign = 1
end
def parse!
while !finished?
case mode
when :start
if scan(SIGN_MARKER)
self.sign = (scanner.matched == "-") ? -1 : 1
self.mode = :sign
else
raise_parsing_error
end
when :sign
if scan(DATE_MARKER)
self.mode = :date
else
raise_parsing_error
end
when :date
if scan(TIME_MARKER)
self.mode = :time
elsif scan(DATE_COMPONENT)
parts[DATE_TO_PART[scanner[2]]] = number * sign
else
raise_parsing_error
end
when :time
if scan(TIME_COMPONENT)
parts[TIME_TO_PART[scanner[2]]] = number * sign
else
raise_parsing_error
end
end
end
validate!
parts
end
private
def finished?
scanner.eos?
end
# Parses number which can be a float with either comma or period.
def number
PERIOD_OR_COMMA.match?(scanner[1]) ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
end
def scan(pattern)
scanner.scan(pattern)
end
def raise_parsing_error(reason = nil)
raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
end
# Checks for various semantic errors as stated in ISO 8601 standard.
def validate!
raise_parsing_error("is empty duration") if parts.empty?
# Mixing any of Y, M, D with W is invalid.
if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
raise_parsing_error("mixing weeks with other date parts not allowed")
end
# Specifying an empty T part is invalid.
if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
raise_parsing_error("time part marker is present but time part is empty")
end
fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
raise_parsing_error "(only last part can be fractional)"
end
true
end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/object/blank"
module ActiveSupport
class Duration
# Serializes duration to string according to ISO 8601 Duration format.
class ISO8601Serializer # :nodoc:
DATE_COMPONENTS = %i(years months days)
def initialize(duration, precision: nil)
@duration = duration
@precision = precision
end
# Builds and returns output string.
def serialize
parts, sign = normalize
return "PT0S" if parts.empty?
output = +"P"
output << "#{parts[:years]}Y" if parts.key?(:years)
output << "#{parts[:months]}M" if parts.key?(:months)
output << "#{parts[:days]}D" if parts.key?(:days)
output << "#{parts[:weeks]}W" if parts.key?(:weeks)
time = +""
time << "#{parts[:hours]}H" if parts.key?(:hours)
time << "#{parts[:minutes]}M" if parts.key?(:minutes)
if parts.key?(:seconds)
time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
end
output << "T#{time}" unless time.empty?
"#{sign}#{output}"
end
private
# Return pair of duration's parts and whole duration sign.
# Parts are summarized (as they can become repetitive due to addition, etc).
# Zero parts are removed as not significant.
# If all parts are negative it will negate all of them and return minus as a sign.
def normalize
parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
p[k] += v unless v.zero?
end
# Convert weeks to days and remove weeks if mixed with date parts
if week_mixed_with_date?(parts)
parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
end
# If all parts are negative - let's make a negative duration
sign = ""
if parts.values.all? { |v| v < 0 }
sign = "-"
parts.transform_values!(&:-@)
end
[parts, sign]
end
def week_mixed_with_date?(parts)
parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
end
end
end
end
# frozen_string_literal: true
- 2
require "yaml"
- 2
require "active_support/encrypted_file"
- 2
require "active_support/ordered_options"
- 2
require "active_support/core_ext/object/inclusion"
- 2
require "active_support/core_ext/module/delegation"
- 2
module ActiveSupport
- 2
class EncryptedConfiguration < EncryptedFile
- 2
delegate :[], :fetch, to: :config
- 2
delegate_missing_to :options
- 2
def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
super content_path: config_path, key_path: key_path,
env_key: env_key, raise_if_missing_key: raise_if_missing_key
end
# Allow a config to be started without a file present
- 2
def read
super
rescue ActiveSupport::EncryptedFile::MissingContentError
""
end
- 2
def write(contents)
deserialize(contents)
super
end
- 2
def config
@config ||= deserialize(read).deep_symbolize_keys
end
- 2
private
- 2
def options
@options ||= ActiveSupport::InheritableOptions.new(config)
end
- 2
def deserialize(config)
YAML.load(config).presence || {}
end
end
end
# frozen_string_literal: true
- 2
require "pathname"
- 2
require "tmpdir"
- 2
require "active_support/message_encryptor"
- 2
module ActiveSupport
- 2
class EncryptedFile
- 2
class MissingContentError < RuntimeError
- 2
def initialize(content_path)
super "Missing encrypted content file in #{content_path}."
end
end
- 2
class MissingKeyError < RuntimeError
- 2
def initialize(key_path:, env_key:)
super \
"Missing encryption key to decrypt file with. " +
"Ask your team for your master key and write it to #{key_path} or put it in the ENV['#{env_key}']."
end
end
- 2
CIPHER = "aes-128-gcm"
- 2
def self.generate_key
SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
end
- 2
attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
- 2
def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
@content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path }
@key_path = Pathname.new(key_path)
@env_key, @raise_if_missing_key = env_key, raise_if_missing_key
end
- 2
def key
read_env_key || read_key_file || handle_missing_key
end
- 2
def read
if !key.nil? && content_path.exist?
decrypt content_path.binread
else
raise MissingContentError, content_path
end
end
- 2
def write(contents)
IO.binwrite "#{content_path}.tmp", encrypt(contents)
FileUtils.mv "#{content_path}.tmp", content_path
end
- 2
def change(&block)
writing read, &block
end
- 2
private
- 2
def writing(contents)
tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
tmp_path.binwrite contents
yield tmp_path
updated_contents = tmp_path.binread
write(updated_contents) if updated_contents != contents
ensure
FileUtils.rm(tmp_path) if tmp_path&.exist?
end
- 2
def encrypt(contents)
encryptor.encrypt_and_sign contents
end
- 2
def decrypt(contents)
encryptor.decrypt_and_verify contents
end
- 2
def encryptor
@encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
end
- 2
def read_env_key
ENV[env_key]
end
- 2
def read_key_file
key_path.binread.strip if key_path.exist?
end
- 2
def handle_missing_key
raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
end
end
end
# frozen_string_literal: true
- 1
require "active_support/string_inquirer"
- 1
module ActiveSupport
- 1
class EnvironmentInquirer < StringInquirer #:nodoc:
- 1
DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
- 1
def initialize(env)
super(env)
DEFAULT_ENVIRONMENTS.each do |default|
instance_variable_set :"@#{default}", env == default
end
end
- 1
DEFAULT_ENVIRONMENTS.each do |env|
- 3
class_eval "def #{env}?; @#{env}; end"
end
end
end
# frozen_string_literal: true
require "set"
require "pathname"
require "concurrent/atomic/atomic_boolean"
require "listen"
module ActiveSupport
# Allows you to "listen" to changes in a file system.
# The evented file updater does not hit disk when checking for updates
# instead it uses platform specific file system events to trigger a change
# in state.
#
# The file checker takes an array of files to watch or a hash specifying directories
# and file extensions to watch. It also takes a block that is called when
# EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
# is run and there have been changes to the file system.
#
# Note: Forking will cause the first call to `updated?` to return `true`.
#
# Example:
#
# checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
# checker.updated?
# # => false
# checker.execute_if_updated
# # => nil
#
# FileUtils.touch("/tmp/foo")
#
# checker.updated?
# # => true
# checker.execute_if_updated
# # => "changed"
#
class EventedFileUpdateChecker #:nodoc: all
def initialize(files, dirs = {}, &block)
unless block
raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
end
@ph = PathHelper.new
@files = files.map { |f| @ph.xpath(f) }.to_set
@dirs = {}
dirs.each do |dir, exts|
@dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
end
@block = block
@updated = Concurrent::AtomicBoolean.new(false)
@lcsp = @ph.longest_common_subpath(@dirs.keys)
@pid = Process.pid
@boot_mutex = Mutex.new
dtw = directories_to_watch
@dtw, @missing = dtw.partition(&:exist?)
boot!
end
def updated?
@boot_mutex.synchronize do
if @pid != Process.pid
boot!
@pid = Process.pid
@updated.make_true
end
end
if @missing.any?(&:exist?)
@boot_mutex.synchronize do
appeared, @missing = @missing.partition(&:exist?)
shutdown!
@dtw += appeared
boot!
@updated.make_true
end
end
@updated.true?
end
def execute
@updated.make_false
@block.call
end
def execute_if_updated
if updated?
yield if block_given?
execute
true
end
end
private
def boot!
normalize_dirs!
Listen.to(*@dtw, &method(:changed)).start if @dtw.any?
end
def shutdown!
Listen.stop
end
def normalize_dirs!
@dirs.transform_keys! do |dir|
dir.exist? ? dir.realpath : dir
end
end
def changed(modified, added, removed)
unless updated?
@updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
end
end
def watching?(file)
file = @ph.xpath(file)
if @files.member?(file)
true
elsif file.directory?
false
else
ext = @ph.normalize_extension(file.extname)
file.dirname.ascend do |dir|
matching = @dirs[dir]
if matching && (matching.empty? || matching.include?(ext))
break true
elsif dir == @lcsp || dir.root?
break false
end
end
end
end
def directories_to_watch
dtw = @files.map(&:dirname) + @dirs.keys
dtw.compact!
dtw.uniq!
normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
dtw = dtw.reject do |path|
normalized_gem_paths.any? { |gem_path| path.to_path.start_with?(gem_path) }
end
@ph.filter_out_descendants(dtw)
end
class PathHelper
def xpath(path)
Pathname.new(path).expand_path
end
def normalize_extension(ext)
ext.to_s.delete_prefix(".")
end
# Given a collection of Pathname objects returns the longest subpath
# common to all of them, or +nil+ if there is none.
def longest_common_subpath(paths)
return if paths.empty?
lcsp = Pathname.new(paths[0])
paths[1..-1].each do |path|
until ascendant_of?(lcsp, path)
if lcsp.root?
# If we get here a root directory is not an ascendant of path.
# This may happen if there are paths in different drives on
# Windows.
return
else
lcsp = lcsp.parent
end
end
end
lcsp
end
# Filters out directories which are descendants of others in the collection (stable).
def filter_out_descendants(dirs)
return dirs if dirs.length < 2
dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
descendants = []
until dirs_sorted_by_nparts.empty?
dir = dirs_sorted_by_nparts.shift
dirs_sorted_by_nparts.reject! do |possible_descendant|
ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
end
end
# Array#- preserves order.
dirs - descendants
end
private
def ascendant_of?(base, other)
base != other && other.ascend do |ascendant|
break true if base == ascendant
end
end
end
end
end
# frozen_string_literal: true
require "active_support/callbacks"
require "concurrent/hash"
module ActiveSupport
class ExecutionWrapper
include ActiveSupport::Callbacks
Null = Object.new # :nodoc:
def Null.complete! # :nodoc:
end
define_callbacks :run
define_callbacks :complete
def self.to_run(*args, &block)
set_callback(:run, *args, &block)
end
def self.to_complete(*args, &block)
set_callback(:complete, *args, &block)
end
RunHook = Struct.new(:hook) do # :nodoc:
def before(target)
hook_state = target.send(:hook_state)
hook_state[hook] = hook.run
end
end
CompleteHook = Struct.new(:hook) do # :nodoc:
def before(target)
hook_state = target.send(:hook_state)
if hook_state.key?(hook)
hook.complete hook_state[hook]
end
end
alias after before
end
# Register an object to be invoked during both the +run+ and
# +complete+ steps.
#
# +hook.complete+ will be passed the value returned from +hook.run+,
# and will only be invoked if +run+ has previously been called.
# (Mostly, this means it won't be invoked if an exception occurs in
# a preceding +to_run+ block; all ordinary +to_complete+ blocks are
# invoked in that situation.)
def self.register_hook(hook, outer: false)
if outer
to_run RunHook.new(hook), prepend: true
to_complete :after, CompleteHook.new(hook)
else
to_run RunHook.new(hook)
to_complete CompleteHook.new(hook)
end
end
# Run this execution.
#
# Returns an instance, whose +complete!+ method *must* be invoked
# after the work has been performed.
#
# Where possible, prefer +wrap+.
def self.run!
if active?
Null
else
new.tap do |instance|
success = nil
begin
instance.run!
success = true
ensure
instance.complete! unless success
end
end
end
end
# Perform the work in the supplied block as an execution.
def self.wrap
return yield if active?
instance = run!
begin
yield
ensure
instance.complete!
end
end
class << self # :nodoc:
attr_accessor :active
end
def self.inherited(other) # :nodoc:
super
other.active = Concurrent::Hash.new
end
self.active = Concurrent::Hash.new
def self.active? # :nodoc:
@active[Thread.current]
end
def run! # :nodoc:
self.class.active[Thread.current] = true
run_callbacks(:run)
end
# Complete this in-flight execution. This method *must* be called
# exactly once on the result of any call to +run!+.
#
# Where possible, prefer +wrap+.
def complete!
run_callbacks(:complete)
ensure
self.class.active.delete Thread.current
end
private
def hook_state
@_hook_state ||= {}
end
end
end
# frozen_string_literal: true
require "active_support/execution_wrapper"
module ActiveSupport
class Executor < ExecutionWrapper
end
end
# frozen_string_literal: true
require "active_support/core_ext/time/calculations"
module ActiveSupport
# FileUpdateChecker specifies the API used by Rails to watch files
# and control reloading. The API depends on four methods:
#
# * +initialize+ which expects two parameters and one block as
# described below.
#
# * +updated?+ which returns a boolean if there were updates in
# the filesystem or not.
#
# * +execute+ which executes the given block on initialization
# and updates the latest watched files and timestamp.
#
# * +execute_if_updated+ which just executes the block if it was updated.
#
# After initialization, a call to +execute_if_updated+ must execute
# the block only if there was really a change in the filesystem.
#
# This class is used by Rails to reload the I18n framework whenever
# they are changed upon a new request.
#
# i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
# I18n.reload!
# end
#
# ActiveSupport::Reloader.to_prepare do
# i18n_reloader.execute_if_updated
# end
class FileUpdateChecker
# It accepts two parameters on initialization. The first is an array
# of files and the second is an optional hash of directories. The hash must
# have directories as keys and the value is an array of extensions to be
# watched under that directory.
#
# This method must also receive a block that will be called once a path
# changes. The array of files and list of directories cannot be changed
# after FileUpdateChecker has been initialized.
def initialize(files, dirs = {}, &block)
unless block
raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
end
@files = files.freeze
@glob = compile_glob(dirs)
@block = block
@watched = nil
@updated_at = nil
@last_watched = watched
@last_update_at = updated_at(@last_watched)
end
# Check if any of the entries were updated. If so, the watched and/or
# updated_at values are cached until the block is executed via +execute+
# or +execute_if_updated+.
def updated?
current_watched = watched
if @last_watched.size != current_watched.size
@watched = current_watched
true
else
current_updated_at = updated_at(current_watched)
if @last_update_at < current_updated_at
@watched = current_watched
@updated_at = current_updated_at
true
else
false
end
end
end
# Executes the given block and updates the latest watched files and
# timestamp.
def execute
@last_watched = watched
@last_update_at = updated_at(@last_watched)
@block.call
ensure
@watched = nil
@updated_at = nil
end
# Execute the block given if updated.
def execute_if_updated
if updated?
yield if block_given?
execute
true
else
false
end
end
private
def watched
@watched || begin
all = @files.select { |f| File.exist?(f) }
all.concat(Dir[@glob]) if @glob
all
end
end
def updated_at(paths)
@updated_at || max_mtime(paths) || Time.at(0)
end
# This method returns the maximum mtime of the files in +paths+, or +nil+
# if the array is empty.
#
# Files with a mtime in the future are ignored. Such abnormal situation
# can happen for example if the user changes the clock by hand. It is
# healthy to consider this edge case because with mtimes in the future
# reloading is not triggered.
def max_mtime(paths)
time_now = Time.now
max_mtime = nil
# Time comparisons are performed with #compare_without_coercion because
# AS redefines these operators in a way that is much slower and does not
# bring any benefit in this particular code.
#
# Read t1.compare_without_coercion(t2) < 0 as t1 < t2.
paths.each do |path|
mtime = File.mtime(path)
next if time_now.compare_without_coercion(mtime) < 0
if max_mtime.nil? || max_mtime.compare_without_coercion(mtime) < 0
max_mtime = mtime
end
end
max_mtime
end
def compile_glob(hash)
hash.freeze # Freeze so changes aren't accidentally pushed
return if hash.empty?
globs = hash.map do |key, value|
"#{escape(key)}/**/*#{compile_ext(value)}"
end
"{#{globs.join(",")}}"
end
def escape(key)
key.gsub(",", '\,')
end
def compile_ext(array)
array = Array(array)
return if array.empty?
".{#{array.join(",")}}"
end
end
end
# frozen_string_literal: true
module ActiveSupport
module ForkTracker # :nodoc:
module CoreExt
def fork(*)
if block_given?
super do
ForkTracker.check!
yield
end
else
unless pid = super
ForkTracker.check!
end
pid
end
end
end
module CoreExtPrivate
include CoreExt
private :fork
end
@pid = Process.pid
@callbacks = []
class << self
def check!
if @pid != Process.pid
@callbacks.each(&:call)
@pid = Process.pid
end
end
def hook!
if Process.respond_to?(:fork)
::Object.prepend(CoreExtPrivate)
::Kernel.prepend(CoreExtPrivate)
::Kernel.singleton_class.prepend(CoreExt)
::Process.singleton_class.prepend(CoreExt)
end
end
def after_fork(&block)
@callbacks << block
block
end
def unregister(callback)
@callbacks.delete(callback)
end
end
end
end
ActiveSupport::ForkTracker.hook!
# frozen_string_literal: true
- 24
module ActiveSupport
# Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>.
- 24
def self.gem_version
Gem::Version.new VERSION::STRING
end
- 24
module VERSION
- 24
MAJOR = 6
- 24
MINOR = 1
- 24
TINY = 0
- 24
PRE = "alpha"
- 24
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
# frozen_string_literal: true
require "zlib"
require "stringio"
module ActiveSupport
# A convenient wrapper for the zlib standard library that allows
# compression/decompression of strings with gzip.
#
# gzip = ActiveSupport::Gzip.compress('compress me!')
# # => "\x1F\x8B\b\x00o\x8D\xCDO\x00\x03K\xCE\xCF-(J-.V\xC8MU\x04\x00R>n\x83\f\x00\x00\x00"
#
# ActiveSupport::Gzip.decompress(gzip)
# # => "compress me!"
module Gzip
class Stream < StringIO
def initialize(*)
super
set_encoding "BINARY"
end
def close; rewind; end
end
# Decompresses a gzipped string.
def self.decompress(source)
Zlib::GzipReader.wrap(StringIO.new(source), &:read)
end
# Compresses a string using gzip.
def self.compress(source, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY)
output = Stream.new
gz = Zlib::GzipWriter.new(output, level, strategy)
gz.write(source)
gz.close
output.string
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/keys"
- 1
require "active_support/core_ext/hash/reverse_merge"
- 1
require "active_support/core_ext/hash/except"
- 1
module ActiveSupport
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
# to be the same.
#
# rgb = ActiveSupport::HashWithIndifferentAccess.new
#
# rgb[:black] = '#000000'
# rgb[:black] # => '#000000'
# rgb['black'] # => '#000000'
#
# rgb['white'] = '#FFFFFF'
# rgb[:white] # => '#FFFFFF'
# rgb['white'] # => '#FFFFFF'
#
# Internally symbols are mapped to strings when used as keys in the entire
# writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This
# mapping belongs to the public interface. For example, given:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
#
# You are guaranteed that the key is returned as a string:
#
# hash.keys # => ["a"]
#
# Technically other types of keys are accepted:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
# hash[0] = 0
# hash # => {"a"=>1, 0=>0}
#
# but this class is intended for use cases where strings or symbols are the
# expected keys and it is convenient to understand both as the same. For
# example the +params+ hash in Ruby on Rails.
#
# Note that core extensions define <tt>Hash#with_indifferent_access</tt>:
#
# rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
#
# which may be handy.
#
# To access this class outside of Rails, require the core extension with:
#
# require "active_support/core_ext/hash/indifferent_access"
#
# which will, in turn, require this file.
- 1
class HashWithIndifferentAccess < Hash
# Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
# this class.
- 1
def extractable_options?
true
end
- 1
def with_indifferent_access
dup
end
- 1
def nested_under_indifferent_access
self
end
- 1
def initialize(constructor = {})
if constructor.respond_to?(:to_hash)
super()
update(constructor)
hash = constructor.is_a?(Hash) ? constructor : constructor.to_hash
self.default = hash.default if hash.default
self.default_proc = hash.default_proc if hash.default_proc
else
super(constructor)
end
end
- 1
def self.[](*args)
new.merge!(Hash[*args])
end
- 1
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
- 1
alias_method :regular_update, :update unless method_defined?(:regular_update)
# Assigns a new value to the hash:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:key] = 'value'
#
# This value can be later fetched using either +:key+ or <tt>'key'</tt>.
- 1
def []=(key, value)
regular_writer(convert_key(key), convert_value(value, conversion: :assignment))
end
- 1
alias_method :store, :[]=
# Updates the receiver in-place, merging in the hashes passed as arguments:
#
# hash_1 = ActiveSupport::HashWithIndifferentAccess.new
# hash_1[:key] = 'value'
#
# hash_2 = ActiveSupport::HashWithIndifferentAccess.new
# hash_2[:key] = 'New Value!'
#
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash.update({ "a" => 1 }, { "b" => 2 }) # => { "a" => 1, "b" => 2 }
#
# The arguments can be either an
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> or a regular +Hash+.
# In either case the merge respects the semantics of indifferent access.
#
# If the argument is a regular hash with keys +:key+ and +"key"+ only one
# of the values end up in the receiver, but which one is unspecified.
#
# When given a block, the value for duplicated keys will be determined
# by the result of invoking the block with the duplicated key, the value
# in the receiver, and the value in +other_hash+. The rules for duplicated
# keys follow the semantics of indifferent access:
#
# hash_1[:key] = 10
# hash_2['key'] = 12
# hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
- 1
def update(*other_hashes, &block)
if other_hashes.size == 1
update_with_single_argument(other_hashes.first, block)
else
other_hashes.each do |other_hash|
update_with_single_argument(other_hash, block)
end
end
self
end
- 1
alias_method :merge!, :update
# Checks the hash for a key matching the argument passed in:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash['key'] = 'value'
# hash.key?(:key) # => true
# hash.key?('key') # => true
- 1
def key?(key)
super(convert_key(key))
end
- 1
alias_method :include?, :key?
- 1
alias_method :has_key?, :key?
- 1
alias_method :member?, :key?
# Same as <tt>Hash#[]</tt> where the key passed as argument can be
# either a string or a symbol:
#
# counters = ActiveSupport::HashWithIndifferentAccess.new
# counters[:foo] = 1
#
# counters['foo'] # => 1
# counters[:foo] # => 1
# counters[:zoo] # => nil
- 1
def [](key)
super(convert_key(key))
end
# Same as <tt>Hash#assoc</tt> where the key passed as argument can be
# either a string or a symbol:
#
# counters = ActiveSupport::HashWithIndifferentAccess.new
# counters[:foo] = 1
#
# counters.assoc('foo') # => ["foo", 1]
# counters.assoc(:foo) # => ["foo", 1]
# counters.assoc(:zoo) # => nil
- 1
def assoc(key)
super(convert_key(key))
end
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
# either a string or a symbol:
#
# counters = ActiveSupport::HashWithIndifferentAccess.new
# counters[:foo] = 1
#
# counters.fetch('foo') # => 1
# counters.fetch(:bar, 0) # => 0
# counters.fetch(:bar) { |key| 0 } # => 0
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
- 1
def fetch(key, *extras)
super(convert_key(key), *extras)
end
# Same as <tt>Hash#dig</tt> where the key passed as argument can be
# either a string or a symbol:
#
# counters = ActiveSupport::HashWithIndifferentAccess.new
# counters[:foo] = { bar: 1 }
#
# counters.dig('foo', 'bar') # => 1
# counters.dig(:foo, :bar) # => 1
# counters.dig(:zoo) # => nil
- 1
def dig(*args)
args[0] = convert_key(args[0]) if args.size > 0
super(*args)
end
# Same as <tt>Hash#default</tt> where the key passed as argument can be
# either a string or a symbol:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new(1)
# hash.default # => 1
#
# hash = ActiveSupport::HashWithIndifferentAccess.new { |hash, key| key }
# hash.default # => nil
# hash.default('foo') # => 'foo'
# hash.default(:foo) # => 'foo'
- 1
def default(*args)
super(*args.map { |arg| convert_key(arg) })
end
# Returns an array of the values at the specified indices:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:a] = 'x'
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
- 1
def values_at(*keys)
super(*keys.map { |key| convert_key(key) })
end
# Returns an array of the values at the specified indices, but also
# raises an exception when one of the keys can't be found.
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash[:a] = 'x'
# hash[:b] = 'y'
# hash.fetch_values('a', 'b') # => ["x", "y"]
# hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
# hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
- 1
def fetch_values(*indices, &block)
super(*indices.map { |key| convert_key(key) }, &block)
end
# Returns a shallow copy of the hash.
#
# hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
# dup = hash.dup
# dup[:a][:c] = 'c'
#
# hash[:a][:c] # => "c"
# dup[:a][:c] # => "c"
- 1
def dup
self.class.new(self).tap do |new_hash|
set_defaults(new_hash)
end
end
# This method has the same semantics of +update+, except it does not
# modify the receiver but rather returns a new hash with indifferent
# access with the result of the merge.
- 1
def merge(*hashes, &block)
dup.update(*hashes, &block)
end
# Like +merge+ but the other way around: Merges the receiver into the
# argument and returns a new hash with indifferent access as result:
#
# hash = ActiveSupport::HashWithIndifferentAccess.new
# hash['a'] = nil
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
- 1
def reverse_merge(other_hash)
super(self.class.new(other_hash))
end
- 1
alias_method :with_defaults, :reverse_merge
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
- 1
def reverse_merge!(other_hash)
super(self.class.new(other_hash))
end
- 1
alias_method :with_defaults!, :reverse_merge!
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
- 1
def replace(other_hash)
super(self.class.new(other_hash))
end
# Removes the specified key from the hash.
- 1
def delete(key)
super(convert_key(key))
end
- 1
def except(*keys)
slice(*self.keys - keys.map { |key| convert_key(key) })
end
- 1
alias_method :without, :except
- 1
def stringify_keys!; self end
- 1
def deep_stringify_keys!; self end
- 1
def stringify_keys; dup end
- 1
def deep_stringify_keys; dup end
- 1
undef :symbolize_keys!
- 1
undef :deep_symbolize_keys!
- 1
def symbolize_keys; to_hash.symbolize_keys! end
- 1
alias_method :to_options, :symbolize_keys
- 1
def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
- 1
def to_options!; self end
- 1
def select(*args, &block)
return to_enum(:select) unless block_given?
dup.tap { |hash| hash.select!(*args, &block) }
end
- 1
def reject(*args, &block)
return to_enum(:reject) unless block_given?
dup.tap { |hash| hash.reject!(*args, &block) }
end
- 1
def transform_values(*args, &block)
return to_enum(:transform_values) unless block_given?
dup.tap { |hash| hash.transform_values!(*args, &block) }
end
- 1
def transform_keys(*args, &block)
return to_enum(:transform_keys) unless block_given?
dup.tap { |hash| hash.transform_keys!(*args, &block) }
end
- 1
def transform_keys!
return enum_for(:transform_keys!) { size } unless block_given?
keys.each do |key|
self[yield(key)] = delete(key)
end
self
end
- 1
def slice(*keys)
keys.map! { |key| convert_key(key) }
self.class.new(super)
end
- 1
def slice!(*keys)
keys.map! { |key| convert_key(key) }
super
end
- 1
def compact
dup.tap(&:compact!)
end
# Convert to a regular hash with string keys.
- 1
def to_hash
_new_hash = Hash.new
set_defaults(_new_hash)
each do |key, value|
_new_hash[key] = convert_value(value, conversion: :to_hash)
end
_new_hash
end
- 1
private
- 1
def convert_key(key)
key.kind_of?(Symbol) ? key.to_s : key
end
- 1
def convert_value(value, conversion: nil)
if value.is_a? Hash
if conversion == :to_hash
value.to_hash
else
value.nested_under_indifferent_access
end
elsif value.is_a?(Array)
if conversion != :assignment || value.frozen?
value = value.dup
end
value.map! { |e| convert_value(e, conversion: conversion) }
else
value
end
end
- 1
def set_defaults(target)
if default_proc
target.default_proc = default_proc.dup
else
target.default = default
end
end
- 1
def update_with_single_argument(other_hash, block)
if other_hash.is_a? HashWithIndifferentAccess
regular_update(other_hash, &block)
else
other_hash.to_hash.each_pair do |key, value|
if block && key?(key)
value = block.call(convert_key(key), self[key], value)
end
regular_writer(convert_key(key), convert_value(value))
end
end
end
end
end
# :stopdoc:
- 1
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
# frozen_string_literal: true
- 24
require "active_support/core_ext/hash/deep_merge"
- 24
require "active_support/core_ext/hash/except"
- 24
require "active_support/core_ext/hash/slice"
- 24
begin
- 24
require "i18n"
rescue LoadError => e
$stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
raise e
end
- 24
require "active_support/lazy_load_hooks"
- 24
ActiveSupport.run_load_hooks(:i18n)
- 24
I18n.load_path << File.expand_path("locale/en.yml", __dir__)
- 24
I18n.load_path << File.expand_path("locale/en.rb", __dir__)
# frozen_string_literal: true
require "active_support"
require "active_support/core_ext/array/wrap"
# :enddoc:
module I18n
class Railtie < Rails::Railtie
config.i18n = ActiveSupport::OrderedOptions.new
config.i18n.railties_load_path = []
config.i18n.load_path = []
config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
config.eager_load_namespaces << I18n
# Set the i18n configuration after initialization since a lot of
# configuration is still usually done in application initializers.
config.after_initialize do |app|
I18n::Railtie.initialize_i18n(app)
end
# Trigger i18n config before any eager loading has happened
# so it's ready if any classes require it when eager loaded.
config.before_eager_load do |app|
I18n::Railtie.initialize_i18n(app)
end
@i18n_inited = false
# Setup i18n configuration.
def self.initialize_i18n(app)
return if @i18n_inited
fallbacks = app.config.i18n.delete(:fallbacks)
# Avoid issues with setting the default_locale by disabling available locales
# check while configuring.
enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
I18n.enforce_available_locales = false
reloadable_paths = []
app.config.i18n.each do |setting, value|
case setting
when :railties_load_path
reloadable_paths = value
app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
when :load_path
I18n.load_path += value
when :raise_on_missing_translations
forward_raise_on_missing_translations_config(app)
else
I18n.send("#{setting}=", value)
end
end
init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
# Restore available locales check so it will take place from now on.
I18n.enforce_available_locales = enforce_available_locales
directories = watched_dirs_with_extensions(reloadable_paths)
reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
I18n.load_path.keep_if { |p| File.exist?(p) }
I18n.load_path |= reloadable_paths.flat_map(&:existent)
end
app.reloaders << reloader
app.reloader.to_run do
reloader.execute_if_updated { require_unload_lock! }
end
reloader.execute
@i18n_inited = true
end
def self.forward_raise_on_missing_translations_config(app)
ActiveSupport.on_load(:action_view) do
self.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
end
ActiveSupport.on_load(:action_controller) do
AbstractController::Translation.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
end
end
def self.include_fallbacks_module
I18n.backend.class.include(I18n::Backend::Fallbacks)
end
def self.init_fallbacks(fallbacks)
include_fallbacks_module
args = \
case fallbacks
when ActiveSupport::OrderedOptions
[*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
when Hash, Array
Array.wrap(fallbacks)
else # TrueClass
[I18n.default_locale]
end
if args.empty? || args.first.is_a?(Hash)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using I18n fallbacks with an empty `defaults` sets the defaults to
include the `default_locale`. This behavior will change in Rails 6.1.
If you desire the default locale to be included in the defaults, please
explicitly configure it with `config.i18n.fallbacks.defaults =
[I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale,
{...}]`. If you want to opt-in to the new behavior, use
`config.i18n.fallbacks.defaults = [nil, {...}]`.
MSG
args.unshift I18n.default_locale
end
I18n.fallbacks = I18n::Locale::Fallbacks.new(*args)
end
def self.validate_fallbacks(fallbacks)
case fallbacks
when ActiveSupport::OrderedOptions
!fallbacks.empty?
when TrueClass, Array, Hash
true
else
raise "Unexpected fallback type #{fallbacks.inspect}"
end
end
def self.watched_dirs_with_extensions(paths)
paths.each_with_object({}) do |path, result|
result[path.absolute_current] = path.extensions
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/inflector/inflections"
#--
# Defines the standard inflection rules. These are the starting point for
# new projects and are not considered complete. The current set of inflection
# rules is frozen. This means, we do not change them to become more complete.
# This is a safety measure to keep existing applications from breaking.
#++
- 24
module ActiveSupport
- 24
Inflector.inflections(:en) do |inflect|
- 24
inflect.plural(/$/, "s")
- 24
inflect.plural(/s$/i, "s")
- 24
inflect.plural(/^(ax|test)is$/i, '\1es')
- 24
inflect.plural(/(octop|vir)us$/i, '\1i')
- 24
inflect.plural(/(octop|vir)i$/i, '\1i')
- 24
inflect.plural(/(alias|status)$/i, '\1es')
- 24
inflect.plural(/(bu)s$/i, '\1ses')
- 24
inflect.plural(/(buffal|tomat)o$/i, '\1oes')
- 24
inflect.plural(/([ti])um$/i, '\1a')
- 24
inflect.plural(/([ti])a$/i, '\1a')
- 24
inflect.plural(/sis$/i, "ses")
- 24
inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
- 24
inflect.plural(/(hive)$/i, '\1s')
- 24
inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
- 24
inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
- 24
inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
- 24
inflect.plural(/^(m|l)ouse$/i, '\1ice')
- 24
inflect.plural(/^(m|l)ice$/i, '\1ice')
- 24
inflect.plural(/^(ox)$/i, '\1en')
- 24
inflect.plural(/^(oxen)$/i, '\1')
- 24
inflect.plural(/(quiz)$/i, '\1zes')
- 24
inflect.singular(/s$/i, "")
- 24
inflect.singular(/(ss)$/i, '\1')
- 24
inflect.singular(/(n)ews$/i, '\1ews')
- 24
inflect.singular(/([ti])a$/i, '\1um')
- 24
inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
- 24
inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
- 24
inflect.singular(/([^f])ves$/i, '\1fe')
- 24
inflect.singular(/(hive)s$/i, '\1')
- 24
inflect.singular(/(tive)s$/i, '\1')
- 24
inflect.singular(/([lr])ves$/i, '\1f')
- 24
inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
- 24
inflect.singular(/(s)eries$/i, '\1eries')
- 24
inflect.singular(/(m)ovies$/i, '\1ovie')
- 24
inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
- 24
inflect.singular(/^(m|l)ice$/i, '\1ouse')
- 24
inflect.singular(/(bus)(es)?$/i, '\1')
- 24
inflect.singular(/(o)es$/i, '\1')
- 24
inflect.singular(/(shoe)s$/i, '\1')
- 24
inflect.singular(/(cris|test)(is|es)$/i, '\1is')
- 24
inflect.singular(/^(a)x[ie]s$/i, '\1xis')
- 24
inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
- 24
inflect.singular(/(alias|status)(es)?$/i, '\1')
- 24
inflect.singular(/^(ox)en/i, '\1')
- 24
inflect.singular(/(vert|ind)ices$/i, '\1ex')
- 24
inflect.singular(/(matr)ices$/i, '\1ix')
- 24
inflect.singular(/(quiz)zes$/i, '\1')
- 24
inflect.singular(/(database)s$/i, '\1')
- 24
inflect.irregular("person", "people")
- 24
inflect.irregular("man", "men")
- 24
inflect.irregular("child", "children")
- 24
inflect.irregular("sex", "sexes")
- 24
inflect.irregular("move", "moves")
- 24
inflect.irregular("zombie", "zombies")
- 24
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
end
end
# frozen_string_literal: true
# in case active_support/inflector is required without the rest of active_support
- 23
require "active_support/inflector/inflections"
- 23
require "active_support/inflector/transliterate"
- 23
require "active_support/inflector/methods"
- 23
require "active_support/inflections"
- 23
require "active_support/core_ext/string/inflections"
# frozen_string_literal: true
- 24
require "concurrent/map"
- 24
require "active_support/i18n"
- 24
module ActiveSupport
- 24
module Inflector
- 24
extend self
# A singleton instance of this class is yielded by Inflector.inflections,
# which can then be used to specify additional inflection rules. If passed
# an optional locale, rules for other languages can be specified. The
# default locale is <tt>:en</tt>. Only rules for English are provided.
#
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1\2en'
# inflect.singular /^(ox)en/i, '\1'
#
# inflect.irregular 'octopus', 'octopi'
#
# inflect.uncountable 'equipment'
# end
#
# New rules are added at the top. So in the example above, the irregular
# rule for octopus will now be the first of the pluralization and
# singularization rules that is runs. This guarantees that your rules run
# before any of the rules that may already have been loaded.
- 24
class Inflections
- 24
@__instance__ = Concurrent::Map.new
- 24
class Uncountables < Array
- 24
def initialize
- 24
@regex_array = []
- 24
super
end
- 24
def delete(entry)
- 2016
super entry
- 2016
@regex_array.delete(to_regex(entry))
end
- 24
def <<(*word)
add(word)
end
- 24
def add(words)
- 25
words = words.flatten.map(&:downcase)
- 25
concat(words)
- 265
@regex_array += words.map { |word| to_regex(word) }
- 25
self
end
- 24
def uncountable?(str)
@regex_array.any? { |regex| regex.match? str }
end
- 24
private
- 24
def to_regex(string)
- 2256
/\b#{::Regexp.escape(string)}\Z/i
end
end
- 24
def self.instance(locale = :en)
- 1753
@__instance__[locale] ||= new
end
- 24
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms
- 24
attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
- 24
def initialize
- 24
@plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
- 24
define_acronym_regex_patterns
end
# Private, for the test suite.
- 24
def initialize_dup(orig) # :nodoc:
%w(plurals singulars uncountables humans acronyms).each do |scope|
instance_variable_set("@#{scope}", orig.send(scope).dup)
end
define_acronym_regex_patterns
end
# Specifies a new acronym. An acronym must be specified as it will appear
# in a camelized string. An underscore string that contains the acronym
# will retain the acronym when passed to +camelize+, +humanize+, or
# +titleize+. A camelized string that contains the acronym will maintain
# the acronym when titleized or humanized, and will convert the acronym
# into a non-delimited single lowercase word when passed to +underscore+.
#
# acronym 'HTML'
# titleize 'html' # => 'HTML'
# camelize 'html' # => 'HTML'
# underscore 'MyHTML' # => 'my_html'
#
# The acronym, however, must occur as a delimited unit and not be part of
# another word for conversions to recognize it:
#
# acronym 'HTTP'
# camelize 'my_http_delimited' # => 'MyHTTPDelimited'
# camelize 'https' # => 'Https', not 'HTTPs'
# underscore 'HTTPS' # => 'http_s', not 'https'
#
# acronym 'HTTPS'
# camelize 'https' # => 'HTTPS'
# underscore 'HTTPS' # => 'https'
#
# Note: Acronyms that are passed to +pluralize+ will no longer be
# recognized, since the acronym will not occur as a delimited unit in the
# pluralized result. To work around this, you must specify the pluralized
# form as an acronym as well:
#
# acronym 'API'
# camelize(pluralize('api')) # => 'Apis'
#
# acronym 'APIs'
# camelize(pluralize('api')) # => 'APIs'
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
# restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
# underscore 'RESTful' # => 'restful'
# underscore 'RESTfulController' # => 'restful_controller'
# titleize 'RESTfulController' # => 'RESTful Controller'
# camelize 'restful' # => 'RESTful'
# camelize 'restful_controller' # => 'RESTfulController'
#
# acronym 'McDonald'
# underscore 'McDonald' # => 'mcdonald'
# camelize 'mcdonald' # => 'McDonald'
- 24
def acronym(word)
@acronyms[word.downcase] = word
define_acronym_regex_patterns
end
# Specifies a new pluralization rule and its replacement. The rule can
# either be a string or a regular expression. The replacement should
# always be a string that may include references to the matched data from
# the rule.
- 24
def plural(rule, replacement)
- 792
@uncountables.delete(rule) if rule.is_a?(String)
- 792
@uncountables.delete(replacement)
- 792
@plurals.prepend([rule, replacement])
end
# Specifies a new singularization rule and its replacement. The rule can
# either be a string or a regular expression. The replacement should
# always be a string that may include references to the matched data from
# the rule.
- 24
def singular(rule, replacement)
- 936
@uncountables.delete(rule) if rule.is_a?(String)
- 936
@uncountables.delete(replacement)
- 936
@singulars.prepend([rule, replacement])
end
# Specifies a new irregular that applies to both pluralization and
# singularization at the same time. This can only be used for strings, not
# regular expressions. You simply pass the irregular in singular and
# plural form.
#
# irregular 'octopus', 'octopi'
# irregular 'person', 'people'
- 24
def irregular(singular, plural)
- 144
@uncountables.delete(singular)
- 144
@uncountables.delete(plural)
- 144
s0 = singular[0]
- 144
srest = singular[1..-1]
- 144
p0 = plural[0]
- 144
prest = plural[1..-1]
- 144
if s0.upcase == p0.upcase
- 144
plural(/(#{s0})#{srest}$/i, '\1' + prest)
- 144
plural(/(#{p0})#{prest}$/i, '\1' + prest)
- 144
singular(/(#{s0})#{srest}$/i, '\1' + srest)
- 144
singular(/(#{p0})#{prest}$/i, '\1' + srest)
else
plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
end
end
# Specifies words that are uncountable and should not be inflected.
#
# uncountable 'money'
# uncountable 'money', 'information'
# uncountable %w( money information rice )
- 24
def uncountable(*words)
- 25
@uncountables.add(words)
end
# Specifies a humanized form of a string by a regular expression rule or
# by a string mapping. When using a regular expression based replacement,
# the normal humanize formatting is called after the replacement. When a
# string is used, the human form should be specified as desired (example:
# 'The name', not 'the_name').
#
# human /_cnt$/i, '\1_count'
# human 'legacy_col_person_name', 'Name'
- 24
def human(rule, replacement)
@humans.prepend([rule, replacement])
end
# Clears the loaded inflections within a given scope (default is
# <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
# options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
# <tt>:humans</tt>.
#
# clear :all
# clear :plurals
- 24
def clear(scope = :all)
case scope
when :all
@plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
else
instance_variable_set "@#{scope}", []
end
end
- 24
private
- 24
def define_acronym_regex_patterns
- 24
@acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
- 24
@acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
- 24
@acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
end
end
# Yields a singleton instance of Inflector::Inflections so you can specify
# additional inflector rules. If passed an optional locale, rules for other
# languages can be specified. If not specified, defaults to <tt>:en</tt>.
# Only rules for English are provided.
#
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.uncountable 'rails'
# end
- 24
def inflections(locale = :en)
- 1753
if block_given?
- 26
yield Inflections.instance(locale)
else
- 1727
Inflections.instance(locale)
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/inflections"
- 24
require "active_support/core_ext/object/blank"
- 24
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
# names, modularized class names to ones without, and class names to foreign
# keys. The default inflections for pluralization, singularization, and
# uncountable words are kept in inflections.rb.
#
# The Rails core team has stated patches for the inflections library will not
# be accepted in order to avoid breaking legacy applications which may be
# relying on errant inflections. If you discover an incorrect inflection and
# require it for your application or wish to define rules for languages other
# than English, please correct or add them yourself (explained below).
- 24
module Inflector
- 24
extend self
# Returns the plural form of the word in the string.
#
# If passed an optional +locale+ parameter, the word will be
# pluralized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# pluralize('post') # => "posts"
# pluralize('octopus') # => "octopi"
# pluralize('sheep') # => "sheep"
# pluralize('words') # => "words"
# pluralize('CamelOctopus') # => "CamelOctopi"
# pluralize('ley', :es) # => "leyes"
- 24
def pluralize(word, locale = :en)
apply_inflections(word, inflections(locale).plurals, locale)
end
# The reverse of #pluralize, returns the singular form of a word in a
# string.
#
# If passed an optional +locale+ parameter, the word will be
# singularized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# singularize('posts') # => "post"
# singularize('octopi') # => "octopus"
# singularize('sheep') # => "sheep"
# singularize('word') # => "word"
# singularize('CamelOctopi') # => "CamelOctopus"
# singularize('leyes', :es) # => "ley"
- 24
def singularize(word, locale = :en)
apply_inflections(word, inflections(locale).singulars, locale)
end
# Converts strings to UpperCamelCase.
# If the +uppercase_first_letter+ parameter is set to false, then produces
# lowerCamelCase.
#
# Also converts '/' to '::' which is useful for converting
# paths to namespaces.
#
# camelize('active_model') # => "ActiveModel"
# camelize('active_model', false) # => "activeModel"
# camelize('active_model/errors') # => "ActiveModel::Errors"
# camelize('active_model/errors', false) # => "activeModel::Errors"
#
# As a rule of thumb you can think of +camelize+ as the inverse of
# #underscore, though there are cases where that does not hold:
#
# camelize(underscore('SSLError')) # => "SslError"
- 24
def camelize(term, uppercase_first_letter = true)
- 3
string = term.to_s
- 3
if uppercase_first_letter
- 6
string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
else
string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
end
- 3
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
- 3
string.gsub!("/", "::")
- 3
string
end
# Makes an underscored, lowercase form from the expression in the string.
#
# Changes '::' to '/' to convert namespaces to paths.
#
# underscore('ActiveModel') # => "active_model"
# underscore('ActiveModel::Errors') # => "active_model/errors"
#
# As a rule of thumb you can think of +underscore+ as the inverse of
# #camelize, though there are cases where that does not hold:
#
# camelize(underscore('SSLError')) # => "SslError"
- 24
def underscore(camel_cased_word)
- 1723
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
- 1723
word = camel_cased_word.to_s.gsub("::", "/")
- 1723
word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
- 1723
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
- 1723
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
- 1723
word.tr!("-", "_")
- 1723
word.downcase!
- 1723
word
end
# Tweaks an attribute name for display to end users.
#
# Specifically, performs these transformations:
#
# * Applies human inflection rules to the argument.
# * Deletes leading underscores, if any.
# * Removes a "_id" suffix if present.
# * Replaces underscores with spaces, if any.
# * Downcases all words except acronyms.
# * Capitalizes the first word.
# The capitalization of the first word can be turned off by setting the
# +:capitalize+ option to false (default is true).
#
# The trailing '_id' can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true (default is false).
#
# humanize('employee_salary') # => "Employee salary"
# humanize('author_id') # => "Author"
# humanize('author_id', capitalize: false) # => "author"
# humanize('_id') # => "Id"
# humanize('author_id', keep_id_suffix: true) # => "Author Id"
#
# If "SSL" was defined to be an acronym:
#
# humanize('ssl_error') # => "SSL error"
#
- 24
def humanize(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.sub!(/\A_+/, "")
unless keep_id_suffix
result.delete_suffix!("_id")
end
result.tr!("_", " ")
result.gsub!(/([a-z\d]*)/i) do |match|
"#{inflections.acronyms[match.downcase] || match.downcase}"
end
if capitalize
result.sub!(/\A\w/) { |match| match.upcase }
end
result
end
# Converts just the first character to uppercase.
#
# upcase_first('what a Lovely Day') # => "What a Lovely Day"
# upcase_first('w') # => "W"
# upcase_first('') # => ""
- 24
def upcase_first(string)
string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
end
# Capitalizes all the words and replaces some characters in the string to
# create a nicer looking title. +titleize+ is meant for creating pretty
# output. It is not used in the Rails internals.
#
# The trailing '_id','Id'.. can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# +titleize+ is also aliased as +titlecase+.
#
# titleize('man from the boondocks') # => "Man From The Boondocks"
# titleize('x-men: the last stand') # => "X Men: The Last Stand"
# titleize('TheManWithoutAPast') # => "The Man Without A Past"
# titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
# titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
- 24
def titleize(word, keep_id_suffix: false)
humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`()])[a-z]/) do |match|
match.capitalize
end
end
# Creates the name of a table like Rails does for models to table names.
# This method uses the #pluralize method on the last word in the string.
#
# tableize('RawScaledScorer') # => "raw_scaled_scorers"
# tableize('ham_and_egg') # => "ham_and_eggs"
# tableize('fancyCategory') # => "fancy_categories"
- 24
def tableize(class_name)
pluralize(underscore(class_name))
end
# Creates a class name from a plural table name like Rails does for table
# names to models. Note that this returns a string and not a Class (To
# convert to an actual class follow +classify+ with #constantize).
#
# classify('ham_and_eggs') # => "HamAndEgg"
# classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
# classify('calculus') # => "Calculu"
- 24
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, "")))
end
# Replaces underscores with dashes in the string.
#
# dasherize('puni_puni') # => "puni-puni"
- 24
def dasherize(underscored_word)
underscored_word.tr("_", "-")
end
# Removes the module part from the expression in the string.
#
# demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
# demodulize('Inflections') # => "Inflections"
# demodulize('::Inflections') # => "Inflections"
# demodulize('') # => ""
#
# See also #deconstantize.
- 24
def demodulize(path)
path = path.to_s
if i = path.rindex("::")
path[(i + 2)..-1]
else
path
end
end
# Removes the rightmost segment from the constant expression in the string.
#
# deconstantize('Net::HTTP') # => "Net"
# deconstantize('::Net::HTTP') # => "::Net"
# deconstantize('String') # => ""
# deconstantize('::String') # => ""
# deconstantize('') # => ""
#
# See also #demodulize.
- 24
def deconstantize(path)
path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
end
# Creates a foreign key name from a class name.
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
#
# foreign_key('Message') # => "message_id"
# foreign_key('Message', false) # => "messageid"
# foreign_key('Admin::Post') # => "post_id"
- 24
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
end
# Tries to find a constant with the name specified in the argument string.
#
# constantize('Module') # => Module
# constantize('Foo::Bar') # => Foo::Bar
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
# account:
#
# C = 'outside'
# module M
# C = 'inside'
# C # => 'inside'
# constantize('C') # => 'outside', same as ::C
# end
#
# NameError is raised when the name is not in CamelCase or the constant is
# unknown.
- 24
def constantize(camel_cased_word)
if camel_cased_word.blank? || !camel_cased_word.include?("::")
Object.const_get(camel_cased_word)
else
names = camel_cased_word.split("::")
# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?
# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?
names.inject(Object) do |constant, name|
if constant == Object
constant.const_get(name)
else
candidate = constant.const_get(name)
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)
# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject(constant) do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const
end
# owner is in Object, so raise
constant.const_get(name, false)
end
end
end
end
# Tries to find a constant with the name specified in the argument string.
#
# safe_constantize('Module') # => Module
# safe_constantize('Foo::Bar') # => Foo::Bar
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
# account:
#
# C = 'outside'
# module M
# C = 'inside'
# C # => 'inside'
# safe_constantize('C') # => 'outside', same as ::C
# end
#
# +nil+ is returned when the name is not in CamelCase or the constant (or
# part of it) is unknown.
#
# safe_constantize('blargle') # => nil
# safe_constantize('UnknownModule') # => nil
# safe_constantize('UnknownModule::Foo::Bar') # => nil
- 24
def safe_constantize(camel_cased_word)
constantize(camel_cased_word)
rescue NameError => e
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
e.name.to_s == camel_cased_word.to_s)
rescue LoadError => e
message = e.respond_to?(:original_message) ? e.original_message : e.message
raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(message)
end
# Returns the suffix that should be added to a number to denote the position
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# ordinal(1) # => "st"
# ordinal(2) # => "nd"
# ordinal(1002) # => "nd"
# ordinal(1003) # => "rd"
# ordinal(-11) # => "th"
# ordinal(-1021) # => "st"
- 24
def ordinal(number)
I18n.translate("number.nth.ordinals", number: number)
end
# Turns a number into an ordinal string used to denote the position in an
# ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# ordinalize(1) # => "1st"
# ordinalize(2) # => "2nd"
# ordinalize(1002) # => "1002nd"
# ordinalize(1003) # => "1003rd"
# ordinalize(-11) # => "-11th"
# ordinalize(-1021) # => "-1021st"
- 24
def ordinalize(number)
I18n.translate("number.nth.ordinalized", number: number)
end
- 24
private
# Mounts a regular expression, returned as a string to ease interpolation,
# that will match part by part the given constant.
#
# const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
# const_regexp("::") # => "::"
- 24
def const_regexp(camel_cased_word)
parts = camel_cased_word.split("::")
return Regexp.escape(camel_cased_word) if parts.blank?
last = parts.pop
parts.reverse!.inject(last) do |acc, part|
part.empty? ? acc : "#{part}(::#{acc})?"
end
end
# Applies inflection rules for +singularize+ and +pluralize+.
#
# If passed an optional +locale+ parameter, the uncountables will be
# found for that locale.
#
# apply_inflections('post', inflections.plurals, :en) # => "posts"
# apply_inflections('posts', inflections.singulars, :en) # => "post"
- 24
def apply_inflections(word, rules, locale = :en)
result = word.to_s.dup
if word.empty? || inflections(locale).uncountables.uncountable?(result)
result
else
rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
result
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/string/multibyte"
- 23
require "active_support/i18n"
- 23
module ActiveSupport
- 23
module Inflector
- 23
ALLOWED_ENCODINGS_FOR_TRANSLITERATE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030].freeze
# Replaces non-ASCII characters with an ASCII approximation, or if none
# exists, a replacement character which defaults to "?".
#
# transliterate('Ærøskøbing')
# # => "AEroskobing"
#
# Default approximations are provided for Western/Latin characters,
# e.g, "ø", "ñ", "é", "ß", etc.
#
# This method is I18n aware, so you can set up custom approximations for a
# locale. This can be useful, for example, to transliterate German's "ü"
# and "ö" to "ue" and "oe", or to add support for transliterating Russian
# to ASCII.
#
# In order to make your custom transliterations available, you must set
# them as the <tt>i18n.transliterate.rule</tt> i18n key:
#
# # Store the transliterations in locales/de.yml
# i18n:
# transliterate:
# rule:
# ü: "ue"
# ö: "oe"
#
# # Or set them using Ruby
# I18n.backend.store_translations(:de, i18n: {
# transliterate: {
# rule: {
# 'ü' => 'ue',
# 'ö' => 'oe'
# }
# }
# })
#
# The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that
# maps characters to ASCII approximations as shown above, or, for more
# complex requirements, a Proc:
#
# I18n.backend.store_translations(:de, i18n: {
# transliterate: {
# rule: ->(string) { MyTransliterator.transliterate(string) }
# }
# })
#
# Now you can have different transliterations for each locale:
#
# transliterate('Jürgen', locale: :en)
# # => "Jurgen"
#
# transliterate('Jürgen', locale: :de)
# # => "Juergen"
#
# Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings
# Other encodings will raise an ArgumentError.
- 23
def transliterate(string, replacement = "?", locale: nil)
string = string.dup if string.frozen?
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
raise ArgumentError, "Cannot transliterate strings with #{string.encoding} encoding" unless ALLOWED_ENCODINGS_FOR_TRANSLITERATE.include?(string.encoding)
input_encoding = string.encoding
# US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if
# US-ASCII is given. This way we can let tidy_bytes handle the string
# in the same way as we do for UTF-8
string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII
# GB18030 is Unicode compatible but is not a direct mapping so needs to be
# transcoded. Using invalid/undef :replace will result in loss of data in
# the event of invalid characters, but since tidy_bytes will replace
# invalid/undef with a "?" we're safe to do the same beforehand
string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030
transliterated = I18n.transliterate(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
replacement: replacement,
locale: locale
)
# Restore the string encoding of the input if it was not UTF-8.
# Apply invalid/undef :replace as tidy_bytes does
transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding
transliterated
end
# Replaces special characters in a string so that it may be used as part of
# a 'pretty' URL.
#
# parameterize("Donald E. Knuth") # => "donald-e-knuth"
# parameterize("^très|Jolie-- ") # => "tres-jolie"
#
# To use a custom separator, override the +separator+ argument.
#
# parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
# parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
#
# To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
# parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
#
# It preserves dashes and underscores unless they are used as separators:
#
# parameterize("^très|Jolie__ ") # => "tres-jolie__"
# parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
# parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
#
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
# the configured <tt>I18n.locale</tt>.
- 23
def parameterize(string, separator: "-", preserve_case: false, locale: nil)
# Replace accented chars with their ASCII equivalents.
parameterized_string = transliterate(string, locale: locale)
# Turn unwanted chars into the separator.
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
unless separator.nil? || separator.empty?
if separator == "-"
re_duplicate_separator = /-{2,}/
re_leading_trailing_separator = /^-|-$/i
else
re_sep = Regexp.escape(separator)
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_separator, separator)
# Remove leading/trailing separator.
parameterized_string.gsub!(re_leading_trailing_separator, "")
end
parameterized_string.downcase! unless preserve_case
parameterized_string
end
end
end
# frozen_string_literal: true
- 2
require "active_support/json/decoding"
- 2
require "active_support/json/encoding"
# frozen_string_literal: true
- 2
require "active_support/core_ext/module/attribute_accessors"
- 2
require "active_support/core_ext/module/delegation"
- 2
require "json"
- 2
module ActiveSupport
# Look for and parse json strings that look like ISO 8601 times.
- 2
mattr_accessor :parse_json_times
- 2
module JSON
# matches YAML-formatted dates
- 2
DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
- 2
DATETIME_REGEX = /\A(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)\z/
- 2
class << self
# Parses a JSON string (JavaScript Object Notation) into a hash.
# See http://www.json.org for more info.
#
# ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
# => {"team" => "rails", "players" => "36"}
- 2
def decode(json)
data = ::JSON.parse(json, quirks_mode: true)
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
data
end
end
# Returns the class of the error that will be raised when there is an
# error in decoding JSON. Using this method means you won't directly
# depend on the ActiveSupport's JSON implementation, in case it changes
# in the future.
#
# begin
# obj = ActiveSupport::JSON.decode(some_string)
# rescue ActiveSupport::JSON.parse_error
# Rails.logger.warn("Attempted to decode invalid JSON: #{some_string}")
# end
- 2
def parse_error
::JSON::ParserError
end
- 2
private
- 2
def convert_dates_from(data)
case data
when nil
nil
when DATE_REGEX
begin
Date.parse(data)
rescue ArgumentError
data
end
when DATETIME_REGEX
begin
Time.zone.parse(data)
rescue ArgumentError
data
end
when Array
data.map! { |d| convert_dates_from(d) }
when Hash
data.each do |key, value|
data[key] = convert_dates_from(value)
end
else
data
end
end
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/object/json"
- 2
require "active_support/core_ext/module/delegation"
- 2
module ActiveSupport
- 2
class << self
- 2
delegate :use_standard_json_time_format, :use_standard_json_time_format=,
:time_precision, :time_precision=,
:escape_html_entities_in_json, :escape_html_entities_in_json=,
:json_encoder, :json_encoder=,
to: :'ActiveSupport::JSON::Encoding'
end
- 2
module JSON
# Dumps objects in JSON (JavaScript Object Notation).
# See http://www.json.org for more info.
#
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
- 2
def self.encode(value, options = nil)
Encoding.json_encoder.new(options).encode(value)
end
- 2
module Encoding #:nodoc:
- 2
class JSONGemEncoder #:nodoc:
- 2
attr_reader :options
- 2
def initialize(options = nil)
@options = options || {}
end
# Encode the given object into a JSON string
- 2
def encode(value)
stringify jsonify value.as_json(options.dup)
end
- 2
private
# Rails does more escaping than the JSON gem natively does (we
# escape \u2028 and \u2029 and optionally >, <, & to work around
# certain browser problems).
- 2
ESCAPED_CHARS = {
"\u2028" => '\u2028',
"\u2029" => '\u2029',
">" => '\u003e',
"<" => '\u003c',
"&" => '\u0026',
}
- 2
ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
- 2
ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u
# This class wraps all the strings we see and does the extra escaping
- 2
class EscapedString < String #:nodoc:
- 2
def to_json(*)
if Encoding.escape_html_entities_in_json
s = super
s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
s
else
s = super
s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
s
end
end
- 2
def to_s
self
end
end
# Mark these as private so we don't leak encoding-specific constructs
- 2
private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
:ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
# Convert an object into a "JSON-ready" representation composed of
# primitives like Hash, Array, String, Numeric,
# and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
#
# This allows developers to implement #as_json without having to
# worry about what base types of objects they are allowed to return
# or having to remember to call #as_json recursively.
#
# Note: the +options+ hash passed to +object.to_json+ is only passed
# to +object.as_json+, not any of this method's recursive +#as_json+
# calls.
- 2
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
value.as_json
when Hash
result = {}
value.each do |k, v|
result[jsonify(k)] = jsonify(v)
end
result
when Array
value.map { |v| jsonify(v) }
else
jsonify value.as_json
end
end
# Encode a "jsonified" Ruby data structure using the JSON gem
- 2
def stringify(jsonified)
::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
end
end
- 2
class << self
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
# to the Active Support legacy format.
- 2
attr_accessor :use_standard_json_time_format
# If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
# as a safety measure.
- 2
attr_accessor :escape_html_entities_in_json
# Sets the precision of encoded time values.
# Defaults to 3 (equivalent to millisecond precision)
- 2
attr_accessor :time_precision
# Sets the encoder used by Rails to encode Ruby objects into JSON strings
# in +Object#to_json+ and +ActiveSupport::JSON.encode+.
- 2
attr_accessor :json_encoder
end
- 2
self.use_standard_json_time_format = true
- 2
self.escape_html_entities_in_json = true
- 2
self.json_encoder = JSONGemEncoder
- 2
self.time_precision = 3
end
end
end
# frozen_string_literal: true
require "concurrent/map"
require "openssl"
module ActiveSupport
# KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2.
# It can be used to derive a number of keys for various purposes from a given secret.
# This lets Rails applications have a single secure secret, but avoid reusing that
# key in multiple incompatible contexts.
class KeyGenerator
def initialize(secret, options = {})
@secret = secret
# The default iterations are higher than required for our key derivation uses
# on the off chance someone uses this for password storage
@iterations = options[:iterations] || 2**16
end
# Returns a derived key suitable for use. The default key_size is chosen
# to be compatible with the default settings of ActiveSupport::MessageVerifier.
# i.e. OpenSSL::Digest::SHA1#block_length
def generate_key(salt, key_size = 64)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
end
end
# CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
# re-executing the key generation process when it's called using the same salt and
# key_size.
class CachingKeyGenerator
def initialize(key_generator)
@key_generator = key_generator
@cache_keys = Concurrent::Map.new
end
# Returns a derived key suitable for use.
def generate_key(*args)
@cache_keys[args.join("|")] ||= @key_generator.generate_key(*args)
end
end
end
# frozen_string_literal: true
- 24
module ActiveSupport
# lazy_load_hooks allows Rails to lazily load a lot of components and thus
# making the app boot faster. Because of this feature now there is no need to
# require <tt>ActiveRecord::Base</tt> at boot time purely to apply
# configuration. Instead a hook is registered that applies configuration once
# <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is
# used as example but this feature can be applied elsewhere too.
#
# Here is an example where +on_load+ method is called to register a hook.
#
# initializer 'active_record.initialize_timezone' do
# ActiveSupport.on_load(:active_record) do
# self.time_zone_aware_attributes = true
# self.default_timezone = :utc
# end
# end
#
# When the entirety of +ActiveRecord::Base+ has been
# evaluated then +run_load_hooks+ is invoked. The very last line of
# +ActiveRecord::Base+ is:
#
# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
- 24
module LazyLoadHooks
- 24
def self.extended(base) # :nodoc:
- 24
base.class_eval do
- 71
@load_hooks = Hash.new { |h, k| h[k] = [] }
- 71
@loaded = Hash.new { |h, k| h[k] = [] }
- 24
@run_once = Hash.new { |h, k| h[k] = [] }
end
end
# Declares a block that will be executed when a Rails component is fully
# loaded.
#
# Options:
#
# * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
# * <tt>:run_once</tt> - Given +block+ will run only once.
- 24
def on_load(name, options = {}, &block)
@loaded[name].each do |base|
execute_hook(name, base, options, block)
end
@load_hooks[name] << [block, options]
end
- 24
def run_load_hooks(name, base = Object)
- 47
@loaded[name] << base
- 47
@load_hooks[name].each do |hook, options|
execute_hook(name, base, options, hook)
end
end
- 24
private
- 24
def with_execution_control(name, block, once)
unless @run_once[name].include?(block)
@run_once[name] << block if once
yield
end
end
- 24
def execute_hook(name, base, options, block)
with_execution_control(name, block, options[:run_once]) do
if options[:yield]
block.call(base)
else
if base.is_a?(Module)
base.class_eval(&block)
else
base.instance_eval(&block)
end
end
end
end
end
- 24
extend LazyLoadHooks
end
# frozen_string_literal: true
{
en: {
number: {
nth: {
ordinals: lambda do |_key, options|
number = options[:number]
case number
when 1; "st"
when 2; "nd"
when 3; "rd"
when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13; "th"
else
num_modulo = number.to_i.abs % 100
num_modulo %= 10 if num_modulo > 13
case num_modulo
when 1; "st"
when 2; "nd"
when 3; "rd"
else "th"
end
end
end,
ordinalized: lambda do |_key, options|
number = options[:number]
"#{number}#{ActiveSupport::Inflector.ordinal(number)}"
end
}
}
}
}
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "active_support/core_ext/class/attribute"
- 1
require "active_support/subscriber"
- 1
module ActiveSupport
# <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume
# <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them.
# The log subscriber dispatches notifications to a registered object based
# on its given namespace.
#
# An example would be Active Record log subscriber responsible for logging
# queries:
#
# module ActiveRecord
# class LogSubscriber < ActiveSupport::LogSubscriber
# def sql(event)
# info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
# end
# end
# end
#
# And it's finally registered as:
#
# ActiveRecord::LogSubscriber.attach_to :active_record
#
# Since we need to know all instance methods before attaching the log
# subscriber, the line above should be called after your
# <tt>ActiveRecord::LogSubscriber</tt> definition.
#
# A logger also needs to be set with <tt>ActiveRecord::LogSubscriber.logger=</tt>.
# This is assigned automatically in a Rails environment.
#
# After configured, whenever a <tt>"sql.active_record"</tt> notification is published,
# it will properly dispatch the event
# (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method.
#
# Being an <tt>ActiveSupport::Notifications</tt> consumer,
# <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if
# instrumented code raises an exception. It is common to log a different
# message in case of an error, and this can be achieved by extending
# the previous example:
#
# module ActiveRecord
# class LogSubscriber < ActiveSupport::LogSubscriber
# def sql(event)
# exception = event.payload[:exception]
#
# if exception
# exception_object = event.payload[:exception_object]
#
# error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \
# "(#{exception_object.backtrace.first})"
# else
# # standard logger code
# end
# end
# end
# end
#
# Log subscriber also has some helpers to deal with logging and automatically
# flushes all logs when the request finishes
# (via <tt>action_dispatch.callback</tt> notification) in a Rails environment.
- 1
class LogSubscriber < Subscriber
# Embed in a String to clear all previous ANSI sequences.
- 1
CLEAR = "\e[0m"
- 1
BOLD = "\e[1m"
# Colors
- 1
BLACK = "\e[30m"
- 1
RED = "\e[31m"
- 1
GREEN = "\e[32m"
- 1
YELLOW = "\e[33m"
- 1
BLUE = "\e[34m"
- 1
MAGENTA = "\e[35m"
- 1
CYAN = "\e[36m"
- 1
WHITE = "\e[37m"
- 1
mattr_accessor :colorize_logging, default: true
- 1
class << self
- 1
def logger
@logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
Rails.logger
end
end
- 1
attr_writer :logger
- 1
def log_subscribers
subscribers
end
# Flush all log_subscribers' logger.
- 1
def flush_all!
logger.flush if logger.respond_to?(:flush)
end
end
- 1
def logger
LogSubscriber.logger
end
- 1
def start(name, id, payload)
super if logger
end
- 1
def finish(name, id, payload)
super if logger
rescue => e
if logger
logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
end
end
- 1
private
- 1
%w(info debug warn error fatal unknown).each do |level|
- 6
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{level}(progname = nil, &block)
logger.#{level}(progname, &block) if logger
end
METHOD
end
# Set color by using a symbol or one of the defined constants. If a third
# option is set to +true+, it also adds bold to the string. This is based
# on the Highline implementation and will automatically append CLEAR to the
# end of the returned String.
- 1
def color(text, color, bold = false) # :doc:
return text unless colorize_logging
color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
bold = bold ? BOLD : ""
"#{bold}#{color}#{text}#{CLEAR}"
end
end
end
# frozen_string_literal: true
- 1
require "active_support/log_subscriber"
- 1
require "active_support/logger"
- 1
require "active_support/notifications"
- 1
module ActiveSupport
- 1
class LogSubscriber
# Provides some helpers to deal with testing log subscribers by setting up
# notifications. Take for instance Active Record subscriber tests:
#
# class SyncLogSubscriberTest < ActiveSupport::TestCase
# include ActiveSupport::LogSubscriber::TestHelper
#
# setup do
# ActiveRecord::LogSubscriber.attach_to(:active_record)
# end
#
# def test_basic_query_logging
# Developer.all.to_a
# wait
# assert_equal 1, @logger.logged(:debug).size
# assert_match(/Developer Load/, @logger.logged(:debug).last)
# assert_match(/SELECT \* FROM "developers"/, @logger.logged(:debug).last)
# end
# end
#
# All you need to do is to ensure that your log subscriber is added to
# Rails::Subscriber, as in the second line of the code above. The test
# helpers are responsible for setting up the queue, subscriptions and
# turning colors in logs off.
#
# The messages are available in the @logger instance, which is a logger with
# limited powers (it actually does not send anything to your output), and
# you can collect them doing @logger.logged(level), where level is the level
# used in logging, like info, debug, warn and so on.
- 1
module TestHelper
- 1
def setup # :nodoc:
@logger = MockLogger.new
@notifier = ActiveSupport::Notifications::Fanout.new
ActiveSupport::LogSubscriber.colorize_logging = false
@old_notifier = ActiveSupport::Notifications.notifier
set_logger(@logger)
ActiveSupport::Notifications.notifier = @notifier
end
- 1
def teardown # :nodoc:
set_logger(nil)
ActiveSupport::Notifications.notifier = @old_notifier
end
- 1
class MockLogger
- 1
include ActiveSupport::Logger::Severity
- 1
attr_reader :flush_count
- 1
attr_accessor :level
- 1
def initialize(level = DEBUG)
@flush_count = 0
@level = level
@logged = Hash.new { |h, k| h[k] = [] }
end
- 1
def method_missing(level, message = nil)
if block_given?
@logged[level] << yield
else
@logged[level] << message
end
end
- 1
def logged(level)
@logged[level].compact.map { |l| l.to_s.strip }
end
- 1
def flush
@flush_count += 1
end
- 1
ActiveSupport::Logger::Severity.constants.each do |severity|
- 6
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{severity.downcase}?
#{severity} >= @level
end
EOT
end
end
# Wait notifications to be published.
- 1
def wait
@notifier.wait
end
# Overwrite if you use another logger in your log subscriber.
#
# def logger
# ActiveRecord::Base.logger = @logger
# end
- 1
def set_logger(logger)
ActiveSupport::LogSubscriber.logger = logger
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/logger_silence"
- 24
require "active_support/logger_thread_safe_level"
- 24
require "logger"
- 24
module ActiveSupport
- 24
class Logger < ::Logger
- 24
include LoggerSilence
# Returns true if the logger destination matches one of the sources
#
# logger = Logger.new(STDOUT)
# ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
# # => true
- 24
def self.logger_outputs_to?(logger, *sources)
logdev = logger.instance_variable_get(:@logdev)
logger_source = logdev.dev if logdev.respond_to?(:dev)
sources.any? { |source| source == logger_source }
end
# Broadcasts logs to multiple loggers.
- 24
def self.broadcast(logger) # :nodoc:
Module.new do
define_method(:add) do |*args, &block|
logger.add(*args, &block)
super(*args, &block)
end
define_method(:<<) do |x|
logger << x
super(x)
end
define_method(:close) do
logger.close
super()
end
define_method(:progname=) do |name|
logger.progname = name
super(name)
end
define_method(:formatter=) do |formatter|
logger.formatter = formatter
super(formatter)
end
define_method(:level=) do |level|
logger.level = level
super(level)
end
define_method(:local_level=) do |level|
logger.local_level = level if logger.respond_to?(:local_level=)
super(level) if respond_to?(:local_level=)
end
define_method(:silence) do |level = Logger::ERROR, &block|
if logger.respond_to?(:silence)
logger.silence(level) do
if defined?(super)
super(level, &block)
else
block.call(self)
end
end
else
if defined?(super)
super(level, &block)
else
block.call(self)
end
end
end
end
end
- 24
def initialize(*args, **kwargs)
super
@formatter = SimpleFormatter.new
end
# Simple formatter which only displays the message.
- 24
class SimpleFormatter < ::Logger::Formatter
# This method is invoked when a log event occurs
- 24
def call(severity, timestamp, progname, msg)
"#{String === msg ? msg : msg.inspect}\n"
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/concern"
- 24
require "active_support/core_ext/module/attribute_accessors"
- 24
require "active_support/logger_thread_safe_level"
- 24
module LoggerSilence
- 24
extend ActiveSupport::Concern
- 24
included do
ActiveSupport::Deprecation.warn(
"Including LoggerSilence is deprecated and will be removed in Rails 6.1. " \
"Please use `ActiveSupport::LoggerSilence` instead"
)
include ActiveSupport::LoggerSilence
end
end
- 24
module ActiveSupport
- 24
module LoggerSilence
- 24
extend ActiveSupport::Concern
- 24
included do
- 26
cattr_accessor :silencer, default: true
- 26
include ActiveSupport::LoggerThreadSafeLevel
end
# Silences the logger for the duration of the block.
- 24
def silence(severity = Logger::ERROR)
silencer ? log_at(severity) { yield self } : yield(self)
end
end
end
# frozen_string_literal: true
- 24
require "active_support/concern"
- 24
require "active_support/core_ext/module/attribute_accessors"
- 24
require "concurrent"
- 24
require "fiber"
- 24
module ActiveSupport
- 24
module LoggerThreadSafeLevel # :nodoc:
- 24
extend ActiveSupport::Concern
- 24
included do
- 26
cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false
end
- 24
Logger::Severity.constants.each do |severity|
- 144
class_eval(<<-EOT, __FILE__, __LINE__ + 1)
def #{severity.downcase}? # def debug?
Logger::#{severity} >= level # DEBUG >= level
end # end
EOT
end
- 24
def after_initialize
ActiveSupport::Deprecation.warn(
"Logger don't need to call #after_initialize directly anymore. It will be deprecated without replacement in " \
"Rails 6.1."
)
end
- 24
def local_log_id
Fiber.current.__id__
end
- 24
def local_level
self.class.local_levels[local_log_id]
end
- 24
def local_level=(level)
case level
when Integer
self.class.local_levels[local_log_id] = level
when Symbol
self.class.local_levels[local_log_id] = Logger::Severity.const_get(level.to_s.upcase)
when nil
self.class.local_levels.delete(local_log_id)
else
raise ArgumentError, "Invalid log level: #{level.inspect}"
end
end
- 24
def level
local_level || super
end
# Change the thread-local level for the duration of the given block.
- 24
def log_at(level)
old_local_level, self.local_level = local_level, level
yield
ensure
self.local_level = old_local_level
end
# Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
# FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
- 24
def add(severity, message = nil, progname = nil, &block) #:nodoc:
severity ||= UNKNOWN
progname ||= @progname
return true if @logdev.nil? || severity < level
if message.nil?
if block_given?
message = yield
else
message = progname
progname = @progname
end
end
@logdev.write \
format_message(format_severity(severity), Time.now, progname, message)
end
end
end
# frozen_string_literal: true
- 2
require "openssl"
- 2
require "base64"
- 2
require "active_support/core_ext/module/attribute_accessors"
- 2
require "active_support/message_verifier"
- 2
require "active_support/messages/metadata"
- 2
module ActiveSupport
# MessageEncryptor is a simple way to encrypt values which get stored
# somewhere you don't trust.
#
# The cipher text and initialization vector are base64 encoded and returned
# to you.
#
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# where you don't want users to be able to determine the value of the payload.
#
# len = ActiveSupport::MessageEncryptor.key_len
# salt = SecureRandom.random_bytes(len)
# key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, len) # => "\x89\xE0\x156\xAC..."
# crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
# encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
# crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
#
# === Confining messages to a specific purpose
#
# By default any message can be used throughout your app. But they can also be
# confined to a specific +:purpose+.
#
# token = crypt.encrypt_and_sign("this is the chair", purpose: :login)
#
# Then that same purpose must be passed when verifying to get the data back out:
#
# crypt.decrypt_and_verify(token, purpose: :login) # => "this is the chair"
# crypt.decrypt_and_verify(token, purpose: :shipping) # => nil
# crypt.decrypt_and_verify(token) # => nil
#
# Likewise, if a message has no purpose it won't be returned when verifying with
# a specific purpose.
#
# token = crypt.encrypt_and_sign("the conversation is lively")
# crypt.decrypt_and_verify(token, purpose: :scare_tactics) # => nil
# crypt.decrypt_and_verify(token) # => "the conversation is lively"
#
# === Making messages expire
#
# By default messages last forever and verifying one year from now will still
# return the original value. But messages can be set to expire at a given
# time with +:expires_in+ or +:expires_at+.
#
# crypt.encrypt_and_sign(parcel, expires_in: 1.month)
# crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year)
#
# Then the messages can be verified and returned up to the expire time.
# Thereafter, verifying returns +nil+.
#
# === Rotating keys
#
# MessageEncryptor also supports rotating out old configurations by falling
# back to a stack of encryptors. Call +rotate+ to build and add an encryptor
# so +decrypt_and_verify+ will also try the fallback.
#
# By default any rotated encryptors use the values of the primary
# encryptor unless specified otherwise.
#
# You'd give your encryptor the new defaults:
#
# crypt = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
#
# Then gradually rotate the old values out by adding them as fallbacks. Any message
# generated with the old values will then work until the rotation is removed.
#
# crypt.rotate old_secret # Fallback to an old secret instead of @secret.
# crypt.rotate cipher: "aes-256-cbc" # Fallback to an old cipher instead of aes-256-gcm.
#
# Though if both the secret and the cipher was changed at the same time,
# the above should be combined into:
#
# crypt.rotate old_secret, cipher: "aes-256-cbc"
- 2
class MessageEncryptor
- 2
prepend Messages::Rotator::Encryptor
- 2
cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
- 2
class << self
- 2
def default_cipher #:nodoc:
if use_authenticated_message_encryption
"aes-256-gcm"
else
"aes-256-cbc"
end
end
end
- 2
module NullSerializer #:nodoc:
- 2
def self.load(value)
value
end
- 2
def self.dump(value)
value
end
end
- 2
module NullVerifier #:nodoc:
- 2
def self.verify(value)
value
end
- 2
def self.generate(value)
value
end
end
- 2
class InvalidMessage < StandardError; end
- 2
OpenSSLCipherError = OpenSSL::Cipher::CipherError
# Initialize a new MessageEncryptor. +secret+ must be at least as long as
# the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
# bits. If you are using a user-entered secret, you can generate a suitable
# key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
# derivation function.
#
# First additional parameter is used as the signature key for +MessageVerifier+.
# This allows you to specify keys to encrypt and sign data.
#
# ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
#
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
# * <tt>:digest</tt> - String of digest to use for signing. Default is
# +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
- 2
def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
@secret = secret
@sign_secret = sign_secret
@cipher = cipher || self.class.default_cipher
@digest = digest || "SHA1" unless aead_mode?
@verifier = resolve_verifier
@serializer = serializer || Marshal
end
# Encrypt and sign a message. We need to sign the message in order to avoid
# padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
- 2
def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
end
# Decrypt and verify a message. We need to verify the message in order to
# avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
- 2
def decrypt_and_verify(data, purpose: nil, **)
_decrypt(verifier.verify(data), purpose)
end
# Given a cipher, returns the key length of the cipher to help generate the key of desired size
- 2
def self.key_len(cipher = default_cipher)
OpenSSL::Cipher.new(cipher).key_len
end
- 2
private
- 2
def _encrypt(value, **metadata_options)
cipher = new_cipher
cipher.encrypt
cipher.key = @secret
# Rely on OpenSSL for the initialization vector
iv = cipher.random_iv
cipher.auth_data = "" if aead_mode?
encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
encrypted_data << cipher.final
blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
blob
end
- 2
def _decrypt(encrypted_message, purpose)
cipher = new_cipher
encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
# Currently the OpenSSL bindings do not raise an error if auth_tag is
# truncated, which would allow an attacker to easily forge it. See
# https://github.com/ruby/openssl/issues/63
raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
cipher.decrypt
cipher.key = @secret
cipher.iv = iv
if aead_mode?
cipher.auth_tag = auth_tag
cipher.auth_data = ""
end
decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final
message = Messages::Metadata.verify(decrypted_data, purpose)
@serializer.load(message) if message
rescue OpenSSLCipherError, TypeError, ArgumentError
raise InvalidMessage
end
- 2
def new_cipher
OpenSSL::Cipher.new(@cipher)
end
- 2
attr_reader :verifier
- 2
def aead_mode?
@aead_mode ||= new_cipher.authenticated?
end
- 2
def resolve_verifier
if aead_mode?
NullVerifier
else
MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
end
end
end
end
# frozen_string_literal: true
- 2
require "base64"
- 2
require "active_support/core_ext/object/blank"
- 2
require "active_support/security_utils"
- 2
require "active_support/messages/metadata"
- 2
require "active_support/messages/rotator"
- 2
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are
# signed to prevent tampering.
#
# This is useful for cases like remember-me tokens and auto-unsubscribe links
# where the session store isn't suitable or available.
#
# Remember Me:
# cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
#
# In the authentication filter:
#
# id, time = @verifier.verify(cookies[:remember_me])
# if Time.now < time
# self.current_user = User.find(id)
# end
#
# By default it uses Marshal to serialize the message. If you want to use
# another serialization method, you can set the serializer in the options
# hash upon initialization:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
#
# +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
# If you want to use a different hash algorithm, you can change it by providing
# +:digest+ key as an option while initializing the verifier:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
#
# === Confining messages to a specific purpose
#
# By default any message can be used throughout your app. But they can also be
# confined to a specific +:purpose+.
#
# token = @verifier.generate("this is the chair", purpose: :login)
#
# Then that same purpose must be passed when verifying to get the data back out:
#
# @verifier.verified(token, purpose: :login) # => "this is the chair"
# @verifier.verified(token, purpose: :shipping) # => nil
# @verifier.verified(token) # => nil
#
# @verifier.verify(token, purpose: :login) # => "this is the chair"
# @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature
# @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature
#
# Likewise, if a message has no purpose it won't be returned when verifying with
# a specific purpose.
#
# token = @verifier.generate("the conversation is lively")
# @verifier.verified(token, purpose: :scare_tactics) # => nil
# @verifier.verified(token) # => "the conversation is lively"
#
# @verifier.verify(token, purpose: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature
# @verifier.verify(token) # => "the conversation is lively"
#
# === Making messages expire
#
# By default messages last forever and verifying one year from now will still
# return the original value. But messages can be set to expire at a given
# time with +:expires_in+ or +:expires_at+.
#
# @verifier.generate(parcel, expires_in: 1.month)
# @verifier.generate(doowad, expires_at: Time.now.end_of_year)
#
# Then the messages can be verified and returned up to the expire time.
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
# <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
#
# === Rotating keys
#
# MessageVerifier also supports rotating out old configurations by falling
# back to a stack of verifiers. Call +rotate+ to build and add a verifier to
# so either +verified+ or +verify+ will also try verifying with the fallback.
#
# By default any rotated verifiers use the values of the primary
# verifier unless specified otherwise.
#
# You'd give your verifier the new defaults:
#
# verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512", serializer: JSON)
#
# Then gradually rotate the old values out by adding them as fallbacks. Any message
# generated with the old values will then work until the rotation is removed.
#
# verifier.rotate old_secret # Fallback to an old secret instead of @secret.
# verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512.
# verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON.
#
# Though the above would most likely be combined into one rotation:
#
# verifier.rotate old_secret, digest: "SHA256", serializer: Marshal
- 2
class MessageVerifier
- 2
prepend Messages::Rotator::Verifier
- 2
class InvalidSignature < StandardError; end
- 2
def initialize(secret, digest: nil, serializer: nil)
raise ArgumentError, "Secret should not be nil." unless secret
@secret = secret
@digest = digest || "SHA1"
@serializer = serializer || Marshal
end
# Checks if a signed message could have been generated by signing an object
# with the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# signed_message = verifier.generate 'a private message'
# verifier.valid_message?(signed_message) # => true
#
# tampered_message = signed_message.chop # editing the message invalidates the signature
# verifier.valid_message?(tampered_message) # => false
- 2
def valid_message?(signed_message)
return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
data, digest = signed_message.split("--")
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
end
# Decodes the signed message using the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
#
# signed_message = verifier.generate 'a private message'
# verifier.verified(signed_message) # => 'a private message'
#
# Returns +nil+ if the message was not signed with the same secret.
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verified(signed_message) # => nil
#
# Returns +nil+ if the message is not Base64-encoded.
#
# invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
# verifier.verified(invalid_message) # => nil
#
# Raises any error raised while decoding the signed message.
#
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
- 2
def verified(signed_message, purpose: nil, **)
if valid_message?(signed_message)
begin
data = signed_message.split("--")[0]
message = Messages::Metadata.verify(decode(data), purpose)
@serializer.load(message) if message
rescue ArgumentError => argument_error
return if argument_error.message.include?("invalid base64")
raise
end
end
end
# Decodes the signed message using the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# signed_message = verifier.generate 'a private message'
#
# verifier.verify(signed_message) # => 'a private message'
#
# Raises +InvalidSignature+ if the message was not signed with the same
# secret or was not Base64-encoded.
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
- 2
def verify(*args, **options)
verified(*args, **options) || raise(InvalidSignature)
end
# Generates a signed message for the provided value.
#
# The message is signed with the +MessageVerifier+'s secret.
# Returns Base64-encoded message joined with the generated signature.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
- 2
def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
"#{data}--#{generate_digest(data)}"
end
- 2
private
- 2
def encode(data)
::Base64.strict_encode64(data)
end
- 2
def decode(data)
::Base64.strict_decode64(data)
end
- 2
def generate_digest(data)
require "openssl" unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end
end
end
# frozen_string_literal: true
- 2
require "time"
- 2
module ActiveSupport
- 2
module Messages #:nodoc:
- 2
class Metadata #:nodoc:
- 2
def initialize(message, expires_at = nil, purpose = nil)
@message, @purpose = message, purpose
@expires_at = expires_at.is_a?(String) ? Time.iso8601(expires_at) : expires_at
end
- 2
def as_json(options = {})
{ _rails: { message: @message, exp: @expires_at, pur: @purpose } }
end
- 2
class << self
- 2
def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
if expires_at || expires_in || purpose
JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
else
message
end
end
- 2
def verify(message, purpose)
extract_metadata(message).verify(purpose)
end
- 2
private
- 2
def pick_expiry(expires_at, expires_in)
if expires_at
expires_at.utc.iso8601(3)
elsif expires_in
Time.now.utc.advance(seconds: expires_in).iso8601(3)
end
end
- 2
def extract_metadata(message)
data = JSON.decode(message) rescue nil
if data.is_a?(Hash) && data.key?("_rails")
new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
else
new(message)
end
end
- 2
def encode(message)
::Base64.strict_encode64(message)
end
- 2
def decode(message)
::Base64.strict_decode64(message)
end
end
- 2
def verify(purpose)
@message if match?(purpose) && fresh?
end
- 2
private
- 2
def match?(purpose)
@purpose.to_s == purpose.to_s
end
- 2
def fresh?
@expires_at.nil? || Time.now.utc < @expires_at
end
end
end
end
# frozen_string_literal: true
- 2
module ActiveSupport
- 2
module Messages
- 2
class RotationConfiguration # :nodoc:
- 2
attr_reader :signed, :encrypted
- 2
def initialize
@signed, @encrypted = [], []
end
- 2
def rotate(kind, *args, **options)
args << options unless options.empty?
case kind
when :signed
@signed << args
when :encrypted
@encrypted << args
end
end
end
end
end
# frozen_string_literal: true
- 2
module ActiveSupport
- 2
module Messages
- 2
module Rotator # :nodoc:
- 2
def initialize(*secrets, on_rotation: nil, **options)
super(*secrets, **options)
@options = options
@rotations = []
@on_rotation = on_rotation
end
- 2
def rotate(*secrets, **options)
@rotations << build_rotation(*secrets, @options.merge(options))
end
- 2
module Encryptor
- 2
include Rotator
- 2
def decrypt_and_verify(*args, on_rotation: @on_rotation, **options)
super
rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
end
- 2
private
- 2
def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
self.class.new(secret, sign_secret, **options)
end
end
- 2
module Verifier
- 2
include Rotator
- 2
def verified(*args, on_rotation: @on_rotation, **options)
super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
end
- 2
private
- 2
def build_rotation(secret = @secret, options)
self.class.new(secret, **options)
end
end
- 2
private
- 2
def run_rotations(on_rotation)
@rotations.find do |rotation|
if message = yield(rotation) rescue next
on_rotation&.call
return message
end
end
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport #:nodoc:
- 23
module Multibyte
- 23
autoload :Chars, "active_support/multibyte/chars"
- 23
autoload :Unicode, "active_support/multibyte/unicode"
# The proxy class returned when calling mb_chars. You can use this accessor
# to configure your own proxy class so you can support other encodings. See
# the ActiveSupport::Multibyte::Chars implementation for an example how to
# do this.
#
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
- 23
def self.proxy_class=(klass)
@proxy_class = klass
end
# Returns the current proxy class.
- 23
def self.proxy_class
@proxy_class ||= ActiveSupport::Multibyte::Chars
end
end
end
# frozen_string_literal: true
require "active_support/json"
require "active_support/core_ext/string/access"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/symbol/starts_ends_with"
require "active_support/core_ext/module/delegation"
module ActiveSupport #:nodoc:
module Multibyte #:nodoc:
# Chars enables you to work transparently with UTF-8 encoding in the Ruby
# String class without having extensive knowledge about the encoding. A
# Chars object accepts a string upon initialization and proxies String
# methods in an encoding safe manner. All the normal String methods are also
# implemented on the proxy.
#
# String methods are proxied through the Chars object, and can be accessed
# 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
# # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
#
# Chars objects are perfectly interchangeable with String objects as long as
# no explicit class checks are made. If certain methods do explicitly check
# the class, call +to_s+ before you pass chars objects to them.
#
# bad.explicit_checking_method 'T'.mb_chars.downcase.to_s
#
# The default Chars implementation assumes that the encoding of the string
# is UTF-8, if you want to handle different encodings you can write your own
# multibyte string handler and configure it through
# ActiveSupport::Multibyte.proxy_class.
#
# class CharsForUTF32
# def size
# @wrapped_string.size / 4
# end
#
# def self.accepts?(string)
# string.length % 4 == 0
# end
# end
#
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
class Chars
include Comparable
attr_reader :wrapped_string
alias to_s wrapped_string
alias to_str wrapped_string
delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
# Creates a new Chars instance by wrapping _string_.
def initialize(string)
@wrapped_string = string
@wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
end
# Forward all undefined methods to the wrapped string.
def method_missing(method, *args, &block)
result = @wrapped_string.__send__(method, *args, &block)
if method.end_with?("!")
self if result
else
result.kind_of?(String) ? chars(result) : result
end
end
# Returns +true+ if _obj_ responds to the given method. Private methods
# are included in the search only if the optional second parameter
# evaluates to +true+.
def respond_to_missing?(method, include_private)
@wrapped_string.respond_to?(method, include_private)
end
# 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
# Works just like <tt>String#split</tt>, with the exception that the items
# in the resulting list are Chars instances instead of String. This makes
# chaining methods easier.
#
# 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
def split(*args)
@wrapped_string.split(*args).map { |i| self.class.new(i) }
end
# Works like <tt>String#slice!</tt>, but returns an instance of
# Chars, or +nil+ if the string was not modified. The string will not be
# modified if the range given is out of bounds
#
# string = 'Welcome'
# string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c">
# string # => 'Welome'
# string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo">
# string # => 'me'
def slice!(*args)
string_sliced = @wrapped_string.slice!(*args)
if string_sliced
chars(string_sliced)
end
end
# Reverses all characters in the string.
#
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
def reverse
chars(@wrapped_string.scan(/\X/).reverse.join)
end
# Limits the byte size of the string to a number of bytes without breaking
# characters. Usable when the storage for a string is limited for some
# reason.
#
# 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
def limit(limit)
chars(@wrapped_string.truncate_bytes(limit, omission: nil))
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) { $1.upcase })
end
alias_method :titlecase, :titleize
# Returns the KC normalization of the string by default. NFKC is
# considered the best normalization form for passing strings to databases
# and validations.
#
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
# <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)
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.
#
# 'é'.length # => 2
# 'é'.mb_chars.decompose.to_s.length # => 3
def decompose
chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
end
# Performs composition on all the characters.
#
# 'é'.length # => 3
# 'é'.mb_chars.compose.to_s.length # => 2
def compose
chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
end
# Returns the number of grapheme clusters in the string.
#
# 'क्षि'.mb_chars.length # => 4
# 'क्षि'.mb_chars.grapheme_length # => 3
def grapheme_length
@wrapped_string.scan(/\X/).length
end
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
# resulting in a valid UTF-8 string.
#
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
# encoding is entirely CP1252 or ISO-8859-1.
def tidy_bytes(force = false)
chars(Unicode.tidy_bytes(@wrapped_string, force))
end
def as_json(options = nil) #:nodoc:
to_s.as_json(options)
end
%w(reverse tidy_bytes).each do |method|
define_method("#{method}!") do |*args|
@wrapped_string = send(method, *args).to_s
self
end
end
private
def chars(string)
self.class.new(string)
end
end
end
end
# frozen_string_literal: true
- 2
module ActiveSupport
- 2
module Multibyte
- 2
module Unicode
- 2
extend self
# A list of all available normalization forms.
# See https://www.unicode.org/reports/tr15/tr15-29.html for more
# information about normalization.
- 2
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
- 2
NORMALIZATION_FORM_ALIASES = { # :nodoc:
c: :nfc,
d: :nfd,
kc: :nfkc,
kd: :nfkd
}
# The Unicode version that is supported by the implementation
- 2
UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
# in NORMALIZATION_FORMS.
#
# ActiveSupport::Multibyte::Unicode.default_normalization_form = :c
- 2
attr_accessor :default_normalization_form
- 2
@default_normalization_form = :kc
# Unpack the string at grapheme boundaries. Returns a list of character
# lists.
#
# Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
# Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
- 2
def unpack_graphemes(string)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be
removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead.
MSG
string.scan(/\X/).map(&:codepoints)
end
# Reverse operation of unpack_graphemes.
#
# Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि'
- 2
def pack_graphemes(unpacked)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be
removed from Rails 6.1. Use array.flatten.pack("U*") instead.
MSG
unpacked.flatten.pack("U*")
end
# Decompose composed characters to the decomposed form.
- 2
def decompose(type, codepoints)
if type == :compatibility
codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
else
codepoints.pack("U*").unicode_normalize(:nfd).codepoints
end
end
# Compose decomposed characters to the composed form.
- 2
def compose(codepoints)
codepoints.pack("U*").unicode_normalize(:nfc).codepoints
end
# Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
- 2
if !defined?(Rubinius)
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
# resulting in a valid UTF-8 string.
#
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
# encoding is entirely CP1252 or ISO-8859-1.
- 2
def tidy_bytes(string, force = false)
return string if string.empty? || string.ascii_only?
return recode_windows1252_chars(string) if force
string.scrub { |bad| recode_windows1252_chars(bad) }
end
else
def tidy_bytes(string, force = false)
return string if string.empty?
return recode_windows1252_chars(string) if force
# We can't transcode to the same format, so we choose a nearly-identical encoding.
# We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
# CP1252 when we get errors. The final string will be 'converted' back to UTF-8
# before returning.
reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
source = string.dup
out = "".force_encoding(Encoding::UTF_16LE)
loop do
reader.primitive_convert(source, out)
_, _, _, error_bytes, _ = reader.primitive_errinfo
break if error_bytes.nil?
out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
reader.finish
out.encode!(Encoding::UTF_8)
end
end
# Returns the KC normalization of the string by default. NFKC is
# considered the best normalization form for passing strings to databases
# and validations.
#
# * <tt>string</tt> - The string to perform normalization on.
# * <tt>form</tt> - The form you want to normalize in. Should be one of
# the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>.
# Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
- 2
def normalize(string, form = nil)
form ||= @default_normalization_form
# 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
- 2
%w(downcase upcase swapcase).each do |method|
- 6
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
string.send(method)
end
end
- 2
private
- 2
def recode_windows1252_chars(string)
string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/notifications/instrumenter"
- 24
require "active_support/notifications/fanout"
- 24
require "active_support/per_thread_registry"
- 24
module ActiveSupport
# = Notifications
#
# <tt>ActiveSupport::Notifications</tt> provides an instrumentation API for
# Ruby.
#
# == Instrumenters
#
# To instrument an event you just need to do:
#
# ActiveSupport::Notifications.instrument('render', extra: :information) do
# render plain: 'Foo'
# end
#
# That first executes the block and then notifies all subscribers once done.
#
# In the example above +render+ is the name of the event, and the rest is called
# the _payload_. The payload is a mechanism that allows instrumenters to pass
# extra information to subscribers. Payloads consist of a hash whose contents
# are arbitrary and generally depend on the event.
#
# == Subscribers
#
# You can consume those events and the information they provide by registering
# a subscriber.
#
# ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
# name # => String, name of the event (such as 'render' from above)
# start # => Time, when the instrumented block started execution
# finish # => Time, when the instrumented block ended execution
# id # => String, unique ID for the instrumenter that fired the event
# payload # => Hash, the payload
# end
#
# Here, the +start+ and +finish+ values represent wall-clock time. If you are
# concerned about accuracy, you can register a monotonic subscriber.
#
# ActiveSupport::Notifications.monotonic_subscribe('render') do |name, start, finish, id, payload|
# name # => String, name of the event (such as 'render' from above)
# start # => Monotonic time, when the instrumented block started execution
# finish # => Monotonic time, when the instrumented block ended execution
# id # => String, unique ID for the instrumenter that fired the event
# payload # => Hash, the payload
# end
#
# The +start+ and +finish+ values above represent monotonic time.
#
# For instance, let's store all "render" events in an array:
#
# events = []
#
# ActiveSupport::Notifications.subscribe('render') do |*args|
# events << ActiveSupport::Notifications::Event.new(*args)
# end
#
# That code returns right away, you are just subscribing to "render" events.
# The block is saved and will be called whenever someone instruments "render":
#
# ActiveSupport::Notifications.instrument('render', extra: :information) do
# render plain: 'Foo'
# end
#
# event = events.first
# event.name # => "render"
# event.duration # => 10 (in milliseconds)
# event.payload # => { extra: :information }
#
# The block in the <tt>subscribe</tt> call gets the name of the event, start
# timestamp, end timestamp, a string with a unique identifier for that event's instrumenter
# (something like "535801666f04d0298cd6"), and a hash with the payload, in
# that order.
#
# If an exception happens during that particular instrumentation the payload will
# have a key <tt>:exception</tt> with an array of two elements as value: a string with
# the name of the exception class, and the exception message.
# The <tt>:exception_object</tt> key of the payload will have the exception
# itself as the value:
#
# event.payload[:exception] # => ["ArgumentError", "Invalid value"]
# event.payload[:exception_object] # => #<ArgumentError: Invalid value>
#
# As the earlier example depicts, the class <tt>ActiveSupport::Notifications::Event</tt>
# is able to take the arguments as they come and provide an object-oriented
# interface to that data.
#
# It is also possible to pass an object which responds to <tt>call</tt> method
# as the second parameter to the <tt>subscribe</tt> method instead of a block:
#
# module ActionController
# class PageRequest
# def call(name, started, finished, unique_id, payload)
# Rails.logger.debug ['notification:', name, started, finished, unique_id, payload].join(' ')
# end
# end
# end
#
# ActiveSupport::Notifications.subscribe('process_action.action_controller', ActionController::PageRequest.new)
#
# resulting in the following output within the logs including a hash with the payload:
#
# notification: process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {
# controller: "Devise::SessionsController",
# action: "new",
# params: {"action"=>"new", "controller"=>"devise/sessions"},
# format: :html,
# method: "GET",
# path: "/login/sign_in",
# status: 200,
# view_runtime: 279.3080806732178,
# db_runtime: 40.053
# }
#
# You can also subscribe to all events whose name matches a certain regexp:
#
# ActiveSupport::Notifications.subscribe(/render/) do |*args|
# ...
# end
#
# and even pass no argument to <tt>subscribe</tt>, in which case you are subscribing
# to all events.
#
# == Temporary Subscriptions
#
# Sometimes you do not want to subscribe to an event for the entire life of
# the application. There are two ways to unsubscribe.
#
# WARNING: The instrumentation framework is designed for long-running subscribers,
# use this feature sparingly because it wipes some internal caches and that has
# a negative impact on performance.
#
# === Subscribe While a Block Runs
#
# You can subscribe to some event temporarily while some block runs. For
# example, in
#
# callback = lambda {|*args| ... }
# ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
# ...
# end
#
# the callback will be called for all "sql.active_record" events instrumented
# during the execution of the block. The callback is unsubscribed automatically
# after that.
#
# To record +started+ and +finished+ values with monotonic time,
# specify the optional <tt>:monotonic</tt> option to the
# <tt>subscribed</tt> method. The <tt>:monotonic</tt> option is set
# to +false+ by default.
#
# callback = lambda {|name, started, finished, unique_id, payload| ... }
# ActiveSupport::Notifications.subscribed(callback, "sql.active_record", monotonic: true) do
# ...
# end
#
# === Manual Unsubscription
#
# The +subscribe+ method returns a subscriber object:
#
# subscriber = ActiveSupport::Notifications.subscribe("render") do |*args|
# ...
# end
#
# To prevent that block from being called anymore, just unsubscribe passing
# that reference:
#
# ActiveSupport::Notifications.unsubscribe(subscriber)
#
# You can also unsubscribe by passing the name of the subscriber object. Note
# that this will unsubscribe all subscriptions with the given name:
#
# ActiveSupport::Notifications.unsubscribe("render")
#
# Subscribers using a regexp or other pattern-matching object will remain subscribed
# to all events that match their original pattern, unless those events match a string
# passed to `unsubscribe`:
#
# subscriber = ActiveSupport::Notifications.subscribe(/render/) { }
# ActiveSupport::Notifications.unsubscribe('render_template.action_view')
# subscriber.matches?('render_template.action_view') # => false
# subscriber.matches?('render_partial.action_view') # => true
#
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
# to all log subscribers. You can use any queue implementation you want.
#
- 24
module Notifications
- 24
class << self
- 24
attr_accessor :notifier
- 24
def publish(name, *args)
notifier.publish(name, *args)
end
- 24
def instrument(name, payload = {})
if notifier.listening?(name)
instrumenter.instrument(name, payload) { yield payload if block_given? }
else
yield payload if block_given?
end
end
# Subscribe to a given event name with the passed +block+.
#
# You can subscribe to events by passing a String to match exact event
# names, or by passing a Regexp to match all events that match a pattern.
#
# ActiveSupport::Notifications.subscribe(/render/) do |*args|
# @event = ActiveSupport::Notifications::Event.new(*args)
# end
#
# The +block+ will receive five parameters with information about the event:
#
# ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
# name # => String, name of the event (such as 'render' from above)
# start # => Time, when the instrumented block started execution
# finish # => Time, when the instrumented block ended execution
# id # => String, unique ID for the instrumenter that fired the event
# payload # => Hash, the payload
# end
#
# If the block passed to the method only takes one parameter,
# it will yield an event object to the block:
#
# ActiveSupport::Notifications.subscribe(/render/) do |event|
# @event = event
# end
- 24
def subscribe(pattern = nil, callback = nil, &block)
- 2
notifier.subscribe(pattern, callback, monotonic: false, &block)
end
- 24
def monotonic_subscribe(pattern = nil, callback = nil, &block)
notifier.subscribe(pattern, callback, monotonic: true, &block)
end
- 24
def subscribed(callback, pattern = nil, monotonic: false, &block)
subscriber = notifier.subscribe(pattern, callback, monotonic: monotonic)
yield
ensure
unsubscribe(subscriber)
end
- 24
def unsubscribe(subscriber_or_name)
notifier.unsubscribe(subscriber_or_name)
end
- 24
def instrumenter
InstrumentationRegistry.instance.instrumenter_for(notifier)
end
end
# This class is a registry which holds all of the +Instrumenter+ objects
# in a particular thread local. To access the +Instrumenter+ object for a
# particular +notifier+, you can call the following method:
#
# InstrumentationRegistry.instrumenter_for(notifier)
#
# The instrumenters for multiple notifiers are held in a single instance of
# this class.
- 24
class InstrumentationRegistry # :nodoc:
- 24
extend ActiveSupport::PerThreadRegistry
- 24
def initialize
@registry = {}
end
- 24
def instrumenter_for(notifier)
@registry[notifier] ||= Instrumenter.new(notifier)
end
end
- 24
self.notifier = Fanout.new
end
end
# frozen_string_literal: true
- 24
require "mutex_m"
- 24
require "concurrent/map"
- 24
require "set"
- 24
require "active_support/core_ext/object/try"
- 24
module ActiveSupport
- 24
module Notifications
# This is a default queue implementation that ships with Notifications.
# It just pushes events to all registered log subscribers.
#
# This class is thread safe. All methods are reentrant.
- 24
class Fanout
- 24
include Mutex_m
- 24
def initialize
- 26
@string_subscribers = Hash.new { |h, k| h[k] = [] }
- 24
@other_subscribers = []
- 24
@listeners_for = Concurrent::Map.new
- 24
super
end
- 24
def subscribe(pattern = nil, callable = nil, monotonic: false, &block)
- 2
subscriber = Subscribers.new(pattern, callable || block, monotonic)
- 2
synchronize do
- 2
if String === pattern
- 2
@string_subscribers[pattern] << subscriber
- 2
@listeners_for.delete(pattern)
else
@other_subscribers << subscriber
@listeners_for.clear
end
end
- 2
subscriber
end
- 24
def unsubscribe(subscriber_or_name)
synchronize do
case subscriber_or_name
when String
@string_subscribers[subscriber_or_name].clear
@listeners_for.delete(subscriber_or_name)
@other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) }
else
pattern = subscriber_or_name.try(:pattern)
if String === pattern
@string_subscribers[pattern].delete(subscriber_or_name)
@listeners_for.delete(pattern)
else
@other_subscribers.delete(subscriber_or_name)
@listeners_for.clear
end
end
end
end
- 24
def start(name, id, payload)
listeners_for(name).each { |s| s.start(name, id, payload) }
end
- 24
def finish(name, id, payload, listeners = listeners_for(name))
listeners.each { |s| s.finish(name, id, payload) }
end
- 24
def publish(name, *args)
listeners_for(name).each { |s| s.publish(name, *args) }
end
- 24
def listeners_for(name)
# 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] ||=
@string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) }
end
end
- 24
def listening?(name)
listeners_for(name).any?
end
# This is a sync queue, so there is no waiting.
- 24
def wait
end
- 24
module Subscribers # :nodoc:
- 24
def self.new(pattern, listener, monotonic)
- 2
subscriber_class = monotonic ? MonotonicTimed : Timed
- 2
if listener.respond_to?(:start) && listener.respond_to?(:finish)
- 2
subscriber_class = Evented
else
# Doing all this to detect a block like `proc { |x| }` vs
# `proc { |*x| }` or `proc { |**x| }`
if listener.respond_to?(:parameters)
params = listener.parameters
if params.length == 1 && params.first.first == :opt
subscriber_class = EventObject
end
end
end
- 2
wrap_all pattern, subscriber_class.new(pattern, listener)
end
- 24
def self.wrap_all(pattern, subscriber)
- 2
unless pattern
AllMessages.new(subscriber)
else
- 2
subscriber
end
end
- 24
class Matcher #:nodoc:
- 24
attr_reader :pattern, :exclusions
- 24
def self.wrap(pattern)
- 2
return pattern if String === pattern
new(pattern)
end
- 24
def initialize(pattern)
@pattern = pattern
@exclusions = Set.new
end
- 24
def unsubscribe!(name)
exclusions << -name if pattern === name
end
- 24
def ===(name)
pattern === name && !exclusions.include?(name)
end
end
- 24
class Evented #:nodoc:
- 24
attr_reader :pattern
- 24
def initialize(pattern, delegate)
- 2
@pattern = Matcher.wrap(pattern)
- 2
@delegate = delegate
- 2
@can_publish = delegate.respond_to?(:publish)
end
- 24
def publish(name, *args)
if @can_publish
@delegate.publish name, *args
end
end
- 24
def start(name, id, payload)
@delegate.start name, id, payload
end
- 24
def finish(name, id, payload)
@delegate.finish name, id, payload
end
- 24
def subscribed_to?(name)
pattern === name
end
- 24
def matches?(name)
pattern && pattern === name
end
- 24
def unsubscribe!(name)
pattern.unsubscribe!(name)
end
end
- 24
class Timed < Evented # :nodoc:
- 24
def publish(name, *args)
@delegate.call name, *args
end
- 24
def start(name, id, payload)
timestack = Thread.current[:_timestack] ||= []
timestack.push Time.now
end
- 24
def finish(name, id, payload)
timestack = Thread.current[:_timestack]
started = timestack.pop
@delegate.call(name, started, Time.now, id, payload)
end
end
- 24
class MonotonicTimed < Evented # :nodoc:
- 24
def publish(name, *args)
@delegate.call name, *args
end
- 24
def start(name, id, payload)
timestack = Thread.current[:_timestack_monotonic] ||= []
timestack.push Concurrent.monotonic_time
end
- 24
def finish(name, id, payload)
timestack = Thread.current[:_timestack_monotonic]
started = timestack.pop
@delegate.call(name, started, Concurrent.monotonic_time, id, payload)
end
end
- 24
class EventObject < Evented
- 24
def start(name, id, payload)
stack = Thread.current[:_event_stack] ||= []
event = build_event name, id, payload
event.start!
stack.push event
end
- 24
def finish(name, id, payload)
stack = Thread.current[:_event_stack]
event = stack.pop
event.payload = payload
event.finish!
@delegate.call event
end
- 24
private
- 24
def build_event(name, id, payload)
ActiveSupport::Notifications::Event.new name, nil, nil, id, payload
end
end
- 24
class AllMessages # :nodoc:
- 24
def initialize(delegate)
@delegate = delegate
end
- 24
def start(name, id, payload)
@delegate.start name, id, payload
end
- 24
def finish(name, id, payload)
@delegate.finish name, id, payload
end
- 24
def publish(name, *args)
@delegate.publish name, *args
end
- 24
def subscribed_to?(name)
true
end
- 24
def unsubscribe!(*)
false
end
- 24
alias :matches? :===
end
end
end
end
end
# frozen_string_literal: true
- 24
require "securerandom"
- 24
module ActiveSupport
- 24
module Notifications
# Instrumenters are stored in a thread local.
- 24
class Instrumenter
- 24
attr_reader :id
- 24
def initialize(notifier)
@id = unique_id
@notifier = notifier
end
# Given a block, instrument it by measuring the time taken to execute
# and publish it. Without a block, simply send a message via the
# notifier. Notice that events get sent even if an error occurs in the
# passed-in block.
- 24
def instrument(name, payload = {})
# some of the listeners might have state
listeners_state = start name, payload
begin
yield payload if block_given?
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise e
ensure
finish_with_state listeners_state, name, payload
end
end
# Send a start notification with +name+ and +payload+.
- 24
def start(name, payload)
@notifier.start name, @id, payload
end
# Send a finish notification with +name+ and +payload+.
- 24
def finish(name, payload)
@notifier.finish name, @id, payload
end
- 24
def finish_with_state(listeners_state, name, payload)
@notifier.finish name, @id, payload, listeners_state
end
- 24
private
- 24
def unique_id
SecureRandom.hex(10)
end
end
- 24
class Event
- 24
attr_reader :name, :time, :end, :transaction_id, :children
- 24
attr_accessor :payload
- 24
def self.clock_gettime_supported? # :nodoc:
- 24
defined?(Process::CLOCK_THREAD_CPUTIME_ID) &&
!Gem.win_platform? &&
!RUBY_PLATFORM.match?(/solaris/i)
end
- 24
private_class_method :clock_gettime_supported?
- 24
def initialize(name, start, ending, transaction_id, payload)
@name = name
@payload = payload.dup
@time = start
@transaction_id = transaction_id
@end = ending
@children = []
@cpu_time_start = 0
@cpu_time_finish = 0
@allocation_count_start = 0
@allocation_count_finish = 0
end
# Record information at the time this event starts
- 24
def start!
@time = now
@cpu_time_start = now_cpu
@allocation_count_start = now_allocations
end
# Record information at the time this event finishes
- 24
def finish!
@cpu_time_finish = now_cpu
@end = now
@allocation_count_finish = now_allocations
end
- 24
def end=(ending)
ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!)
@end = ending
end
# Returns the CPU time (in milliseconds) passed since the call to
# +start!+ and the call to +finish!+
- 24
def cpu_time
(@cpu_time_finish - @cpu_time_start) * 1000
end
# Returns the idle time time (in milliseconds) passed since the call to
# +start!+ and the call to +finish!+
- 24
def idle_time
duration - cpu_time
end
# Returns the number of allocations made since the call to +start!+ and
# the call to +finish!+
- 24
def allocations
@allocation_count_finish - @allocation_count_start
end
# Returns the difference in milliseconds between when the execution of the
# event started and when it ended.
#
# ActiveSupport::Notifications.subscribe('wait') do |*args|
# @event = ActiveSupport::Notifications::Event.new(*args)
# end
#
# ActiveSupport::Notifications.instrument('wait') do
# sleep 1
# end
#
# @event.duration # => 1000.138
- 24
def duration
1000.0 * (self.end - time)
end
- 24
def <<(event)
@children << event
end
- 24
def parent_of?(event)
@children.include? event
end
- 24
private
- 24
def now
Concurrent.monotonic_time
end
- 24
if clock_gettime_supported?
- 24
def now_cpu
Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
end
else
def now_cpu
0
end
end
- 24
if defined?(JRUBY_VERSION)
def now_allocations
0
end
else
- 24
def now_allocations
GC.stat :total_allocated_objects
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActiveSupport
- 1
module NumberHelper
- 1
extend ActiveSupport::Autoload
- 1
eager_autoload do
- 1
autoload :NumberConverter
- 1
autoload :RoundingHelper
- 1
autoload :NumberToRoundedConverter
- 1
autoload :NumberToDelimitedConverter
- 1
autoload :NumberToHumanConverter
- 1
autoload :NumberToHumanSizeConverter
- 1
autoload :NumberToPhoneConverter
- 1
autoload :NumberToCurrencyConverter
- 1
autoload :NumberToPercentageConverter
end
- 1
extend self
# Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:area_code</tt> - Adds parentheses around the area code.
# * <tt>:delimiter</tt> - Specifies the delimiter to use
# (defaults to "-").
# * <tt>:extension</tt> - Specifies an extension to add to the
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
# * <tt>:pattern</tt> - Specifies how the number is divided into three
# groups with the custom regexp to override the default format.
# ==== Examples
#
# number_to_phone(5551234) # => "555-1234"
# number_to_phone('5551234') # => "555-1234"
# number_to_phone(1235551234) # => "123-555-1234"
# number_to_phone(1235551234, area_code: true) # => "(123) 555-1234"
# number_to_phone(1235551234, delimiter: ' ') # => "123 555 1234"
# number_to_phone(1235551234, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
# number_to_phone(1235551234, country_code: 1) # => "+1-123-555-1234"
# number_to_phone('123a456') # => "123a456"
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
# # => "+1.123.555.1234 x 1343"
#
# number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
# # => "(755) 6123-4567"
# number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
# # => "133-1234-5678"
- 1
def number_to_phone(number, options = {})
NumberToPhoneConverter.convert(number, options)
end
# Formats a +number+ into a currency string (e.g., $13.65). You
# can customize the format in the +options+ hash.
#
# The currency unit and number formatting of the current locale will be used
# unless otherwise specified in the provided options. No currency conversion
# is performed. If the user is given a way to change their locale, they will
# also be able to change the relative value of the currency displayed with
# this helper. If your application will ever support multiple locales, you
# may want to specify a constant <tt>:locale</tt> option or consider
# using a library capable of currency conversion.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the level of precision (defaults
# to 2).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:unit</tt> - Sets the denomination of the currency
# (defaults to "$").
# * <tt>:separator</tt> - Sets the separator between the units
# (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to ",").
# * <tt>:format</tt> - Sets the format for non-negative numbers
# (defaults to "%u%n"). Fields are <tt>%u</tt> for the
# currency, and <tt>%n</tt> for the number.
# * <tt>:negative_format</tt> - Sets the format for negative
# numbers (defaults to prepending a hyphen to the formatted
# number given by <tt>:format</tt>). Accepts the same fields
# than <tt>:format</tt>, except <tt>%n</tt> is here the
# absolute value of the number.
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
#
# ==== Examples
#
# number_to_currency(1234567890.50) # => "$1,234,567,890.50"
# number_to_currency(1234567890.506) # => "$1,234,567,890.51"
# number_to_currency(1234567890.506, precision: 3) # => "$1,234,567,890.506"
# number_to_currency(1234567890.506, locale: :fr) # => "1 234 567 890,51 €"
# number_to_currency('123a456') # => "$123a456"
#
# number_to_currency("123a456", raise: true) # => InvalidNumberError
#
# number_to_currency(-0.456789, precision: 0)
# # => "$0"
# number_to_currency(-1234567890.50, negative_format: '(%u%n)')
# # => "($1,234,567,890.50)"
# number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '')
# # => "£1234567890,50"
# number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u')
# # => "1234567890,50 £"
# number_to_currency(1234567890.50, strip_insignificant_zeros: true)
# # => "$1,234,567,890.5"
# number_to_currency(1234567890.50, precision: 0, round_mode: :up)
# # => "$1,234,567,891"
- 1
def number_to_currency(number, options = {})
NumberToCurrencyConverter.convert(number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
# customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
# * <tt>:format</tt> - Specifies the format of the percentage
# string The number field is <tt>%n</tt> (defaults to "%n%").
#
# ==== Examples
#
# number_to_percentage(100) # => "100.000%"
# number_to_percentage('98') # => "98.000%"
# number_to_percentage(100, precision: 0) # => "100%"
# number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
# number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
# number_to_percentage(1000, locale: :fr) # => "1000,000%"
# number_to_percentage(1000, precision: nil) # => "1000%"
# number_to_percentage('98a') # => "98a%"
# number_to_percentage(100, format: '%n %') # => "100.000 %"
# number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
- 1
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
# (e.g., 12,324). You can customize the format in the +options+
# hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to ",").
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for
# deriving the placement of delimiter. Helpful when using currency formats
# like INR.
#
# ==== Examples
#
# number_to_delimited(12345678) # => "12,345,678"
# number_to_delimited('123456') # => "123,456"
# number_to_delimited(12345678.05) # => "12,345,678.05"
# number_to_delimited(12345678, delimiter: '.') # => "12.345.678"
# number_to_delimited(12345678, delimiter: ',') # => "12,345,678"
# number_to_delimited(12345678.05, separator: ' ') # => "12,345,678 05"
# number_to_delimited(12345678.05, locale: :fr) # => "12 345 678,05"
# number_to_delimited('112a') # => "112a"
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
# # => "98 765 432,98"
# number_to_delimited("123456.78",
# delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/)
# # => "1,23,456.78"
- 1
def number_to_delimited(number, options = {})
NumberToDelimitedConverter.convert(number, options)
end
# Formats a +number+ with the specified level of
# <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
# +:significant+ is +false+, and 5 if +:significant+ is +true+).
# You can customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3). Keeps the number's precision if +nil+.
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
#
# ==== Examples
#
# number_to_rounded(111.2345) # => "111.235"
# number_to_rounded(111.2345, precision: 2) # => "111.23"
# number_to_rounded(13, precision: 5) # => "13.00000"
# number_to_rounded(389.32314, precision: 0) # => "389"
# number_to_rounded(111.2345, significant: true) # => "111"
# number_to_rounded(111.2345, precision: 1, significant: true) # => "100"
# number_to_rounded(13, precision: 5, significant: true) # => "13.000"
# number_to_rounded(13, precision: nil) # => "13"
# number_to_rounded(389.32314, precision: 0, round_mode: :up) # => "390"
# number_to_rounded(111.234, locale: :fr) # => "111,234"
#
# number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
# # => "13"
#
# number_to_rounded(389.32314, precision: 4, significant: true) # => "389.3"
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => "1.111,23"
- 1
def number_to_rounded(number, options = {})
NumberToRoundedConverter.convert(number, options)
end
# Formats the bytes in +number+ into a more understandable
# representation (e.g., giving it 1500 yields 1.46 KB). This
# method is useful for reporting file sizes to users. You can
# customize the format in the +options+ hash.
#
# See <tt>number_to_human</tt> if you want to pretty-print a
# generic number.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
#
# ==== Examples
#
# number_to_human_size(123) # => "123 Bytes"
# number_to_human_size(1234) # => "1.21 KB"
# number_to_human_size(12345) # => "12.1 KB"
# number_to_human_size(1234567) # => "1.18 MB"
# number_to_human_size(1234567890) # => "1.15 GB"
# number_to_human_size(1234567890123) # => "1.12 TB"
# number_to_human_size(1234567890123456) # => "1.1 PB"
# number_to_human_size(1234567890123456789) # => "1.07 EB"
# number_to_human_size(1234567, precision: 2) # => "1.2 MB"
# number_to_human_size(483989, precision: 2) # => "470 KB"
# number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
# number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB"
# number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
- 1
def number_to_human_size(number, options = {})
NumberToHumanSizeConverter.convert(number, options)
end
# Pretty prints (formats and approximates) a number in a way it
# is more readable by humans (e.g.: 1200000000 becomes "1.2
# Billion"). This is useful for numbers that can get very large
# (and too hard to read).
#
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
# You can also define your own unit-quantifier names if you want
# to use other decimal units (e.g.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
# (centi, deci, mili, etc).
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:round_mode</tt> - Determine how rounding is performed
# (defaults to :default. See BigDecimal::mode)
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
# * <tt>:units</tt> - A Hash of unit quantifier names. Or a
# string containing an i18n scope where to find this hash. It
# might have the following keys:
# * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
# <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
# <tt>:billion</tt>, <tt>:trillion</tt>,
# <tt>:quadrillion</tt>
# * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
# <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
# <tt>:pico</tt>, <tt>:femto</tt>
# * <tt>:format</tt> - Sets the format of the output string
# (defaults to "%n %u"). The field types are:
# * %u - The quantifier (ex.: 'thousand')
# * %n - The number
#
# ==== Examples
#
# number_to_human(123) # => "123"
# number_to_human(1234) # => "1.23 Thousand"
# number_to_human(12345) # => "12.3 Thousand"
# number_to_human(1234567) # => "1.23 Million"
# number_to_human(1234567890) # => "1.23 Billion"
# number_to_human(1234567890123) # => "1.23 Trillion"
# number_to_human(1234567890123456) # => "1.23 Quadrillion"
# number_to_human(1234567890123456789) # => "1230 Quadrillion"
# number_to_human(489939, precision: 2) # => "490 Thousand"
# number_to_human(489939, precision: 4) # => "489.9 Thousand"
# number_to_human(489939, precision: 2
# , round_mode: :down) # => "480 Thousand"
# number_to_human(1234567, precision: 4,
# significant: false) # => "1.2346 Million"
# number_to_human(1234567, precision: 1,
# separator: ',',
# significant: false) # => "1,2 Million"
#
# number_to_human(500000000, precision: 5) # => "500 Million"
# number_to_human(12345012345, significant: false) # => "12.345 Billion"
#
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
#
# number_to_human(12.00001) # => "12"
# number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
# You can also use your own custom unit quantifiers:
# number_to_human(500000, units: { unit: 'ml', thousand: 'lt' }) # => "500 lt"
#
# If in your I18n locale you have:
#
# distance:
# centi:
# one: "centimeter"
# other: "centimeters"
# unit:
# one: "meter"
# other: "meters"
# thousand:
# one: "kilometer"
# other: "kilometers"
# billion: "gazillion-distance"
#
# Then you could do:
#
# number_to_human(543934, units: :distance) # => "544 kilometers"
# number_to_human(54393498, units: :distance) # => "54400 kilometers"
# number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance"
# number_to_human(343, units: :distance, precision: 1) # => "300 meters"
# number_to_human(1, units: :distance) # => "1 meter"
# number_to_human(0.34, units: :distance) # => "34 centimeters"
- 1
def number_to_human(number, options = {})
NumberToHumanConverter.convert(number, options)
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/big_decimal/conversions"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/hash/keys"
require "active_support/i18n"
require "active_support/core_ext/class/attribute"
module ActiveSupport
module NumberHelper
class NumberConverter # :nodoc:
# Default and i18n option namespace per class
class_attribute :namespace
# Does the object need a number that is a valid float?
class_attribute :validate_float
attr_reader :number, :opts
DEFAULTS = {
# Used in number_to_delimited
# These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
format: {
# Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
separator: ".",
# Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
delimiter: ",",
# Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
precision: 3,
# If set to true, precision will mean the number of significant digits instead
# of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
significant: false,
# If set, the zeros after the decimal separator will always be stripped (e.g.: 1.200 will be 1.2)
strip_insignificant_zeros: false
},
# Used in number_to_currency
currency: {
format: {
format: "%u%n",
negative_format: "-%u%n",
unit: "$",
# These five are to override number.format and are optional
separator: ".",
delimiter: ",",
precision: 2,
significant: false,
strip_insignificant_zeros: false
}
},
# Used in number_to_percentage
percentage: {
format: {
delimiter: "",
format: "%n%"
}
},
# Used in number_to_rounded
precision: {
format: {
delimiter: ""
}
},
# Used in number_to_human_size and number_to_human
human: {
format: {
# These five are to override number.format and are optional
delimiter: "",
precision: 3,
significant: true,
strip_insignificant_zeros: true
},
# Used in number_to_human_size
storage_units: {
# Storage units output formatting.
# %u is the storage unit, %n is the number (default: 2 MB)
format: "%n %u",
units: {
byte: "Bytes",
kb: "KB",
mb: "MB",
gb: "GB",
tb: "TB"
}
},
# Used in number_to_human
decimal_units: {
format: "%n %u",
# Decimal units output formatting
# By default we will only quantify some of the exponents
# but the commented ones might be defined or overridden
# by the user.
units: {
# femto: Quadrillionth
# pico: Trillionth
# nano: Billionth
# micro: Millionth
# mili: Thousandth
# centi: Hundredth
# deci: Tenth
unit: "",
# ten:
# one: Ten
# other: Tens
# hundred: Hundred
thousand: "Thousand",
million: "Million",
billion: "Billion",
trillion: "Trillion",
quadrillion: "Quadrillion"
}
}
}
}
def self.convert(number, options)
new(number, options).execute
end
def initialize(number, options)
@number = number
@opts = options.symbolize_keys
end
def execute
if !number
nil
elsif validate_float? && !valid_float?
number
else
convert
end
end
private
def options
@options ||= format_options.merge(opts)
end
def format_options
default_format_options.merge!(i18n_format_options)
end
def default_format_options
options = DEFAULTS[:format].dup
options.merge!(DEFAULTS[namespace][:format]) if namespace
options
end
def i18n_format_options
locale = opts[:locale]
options = I18n.translate(:'number.format', locale: locale, default: {}).dup
if namespace
options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
end
options
end
def translate_number_value_with_default(key, **i18n_options)
I18n.translate(key, **{ default: default_value(key), scope: :number }.merge!(i18n_options))
end
def translate_in_locale(key, **i18n_options)
translate_number_value_with_default(key, **{ locale: options[:locale] }.merge(i18n_options))
end
def default_value(key)
key.split(".").reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
end
def valid_float?
Float(number)
rescue ArgumentError, TypeError
false
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToCurrencyConverter < NumberConverter # :nodoc:
self.namespace = :currency
def convert
number = self.number.to_s.strip
number_f = number.to_f
format = options[:format]
if number_f.negative?
number = number_f.abs
unless options[:precision] == 0 && number < 0.5
format = options[:negative_format]
end
end
rounded_number = NumberToRoundedConverter.convert(number, options)
format.gsub("%n", rounded_number).gsub("%u", options[:unit])
end
private
def options
@options ||= begin
defaults = default_format_options.merge(i18n_opts)
# Override negative format if format options are given
defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
defaults.merge!(opts)
end
end
def i18n_opts
# Set International negative format if it does not exist
i18n = i18n_format_options
i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
i18n
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToDelimitedConverter < NumberConverter #:nodoc:
self.validate_float = true
DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
def convert
parts.join(options[:separator])
end
private
def parts
left, right = number.to_s.split(".")
left.gsub!(delimiter_pattern) do |digit_to_delimit|
"#{digit_to_delimit}#{options[:delimiter]}"
end
[left, right].compact
end
def delimiter_pattern
options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX)
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToHumanConverter < NumberConverter # :nodoc:
DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
-1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
self.namespace = :human
self.validate_float = true
def convert # :nodoc:
@number = RoundingHelper.new(options).round(number)
@number = Float(number)
# For backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files.
unless options.key?(:strip_insignificant_zeros)
options[:strip_insignificant_zeros] = true
end
units = opts[:units]
exponent = calculate_exponent(units)
@number = number / (10**exponent)
rounded_number = NumberToRoundedConverter.convert(number, options)
unit = determine_unit(units, exponent)
format.gsub("%n", rounded_number).gsub("%u", unit).strip
end
private
def format
options[:format] || translate_in_locale("human.decimal_units.format")
end
def determine_unit(units, exponent)
exp = DECIMAL_UNITS[exponent]
case units
when Hash
units[exp] || ""
when String, Symbol
I18n.translate("#{units}.#{exp}", locale: options[:locale], count: number.to_i)
else
translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
end
end
def calculate_exponent(units)
exponent = number != 0 ? Math.log10(number.abs).floor : 0
unit_exponents(units).find { |e| exponent >= e } || 0
end
def unit_exponents(units)
case units
when Hash
units
when String, Symbol
I18n.translate(units.to_s, locale: options[:locale], raise: true)
when nil
translate_in_locale("human.decimal_units.units", raise: true)
else
raise ArgumentError, ":units must be a Hash or String translation scope."
end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToHumanSizeConverter < NumberConverter #:nodoc:
STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb]
self.namespace = :human
self.validate_float = true
def convert
@number = Float(number)
# For backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files.
unless options.key?(:strip_insignificant_zeros)
options[:strip_insignificant_zeros] = true
end
if smaller_than_base?
number_to_format = number.to_i.to_s
else
human_size = number / (base**exponent)
number_to_format = NumberToRoundedConverter.convert(human_size, options)
end
conversion_format.gsub("%n", number_to_format).gsub("%u", unit)
end
private
def conversion_format
translate_number_value_with_default("human.storage_units.format", locale: options[:locale], raise: true)
end
def unit
translate_number_value_with_default(storage_unit_key, locale: options[:locale], count: number.to_i, raise: true)
end
def storage_unit_key
key_end = smaller_than_base? ? "byte" : STORAGE_UNITS[exponent]
"human.storage_units.units.#{key_end}"
end
def exponent
max = STORAGE_UNITS.size - 1
exp = (Math.log(number) / Math.log(base)).to_i
exp = max if exp > max # avoid overflow for the highest unit
exp
end
def smaller_than_base?
number.to_i < base
end
def base
1024
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToPercentageConverter < NumberConverter # :nodoc:
self.namespace = :percentage
def convert
rounded_number = NumberToRoundedConverter.convert(number, options)
options[:format].gsub("%n", rounded_number)
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToPhoneConverter < NumberConverter #:nodoc:
def convert
str = country_code(opts[:country_code]).dup
str << convert_to_phone_number(number.to_s.strip)
str << phone_ext(opts[:extension])
end
private
def convert_to_phone_number(number)
if opts[:area_code]
convert_with_area_code(number)
else
convert_without_area_code(number)
end
end
def convert_with_area_code(number)
default_pattern = /(\d{1,3})(\d{3})(\d{4}$)/
number.gsub!(regexp_pattern(default_pattern),
"(\\1) \\2#{delimiter}\\3")
number
end
def convert_without_area_code(number)
default_pattern = /(\d{0,3})(\d{3})(\d{4})$/
number.gsub!(regexp_pattern(default_pattern),
"\\1#{delimiter}\\2#{delimiter}\\3")
number.slice!(0, 1) if start_with_delimiter?(number)
number
end
def start_with_delimiter?(number)
delimiter.present? && number.start_with?(delimiter)
end
def delimiter
opts[:delimiter] || "-"
end
def country_code(code)
code.blank? ? "" : "+#{code}#{delimiter}"
end
def phone_ext(ext)
ext.blank? ? "" : " x #{ext}"
end
def regexp_pattern(default_pattern)
opts.fetch :pattern, default_pattern
end
end
end
end
# frozen_string_literal: true
require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
class NumberToRoundedConverter < NumberConverter # :nodoc:
self.namespace = :precision
self.validate_float = true
def convert
helper = RoundingHelper.new(options)
rounded_number = helper.round(number)
if precision = options[:precision]
if options[:significant] && precision > 0
digits = helper.digit_count(rounded_number)
precision -= digits
precision = 0 if precision < 0 # don't let it be negative
end
formatted_string =
if rounded_number.nan? || rounded_number.infinite? || rounded_number == rounded_number.to_i
"%00.#{precision}f" % rounded_number
else
s = rounded_number.to_s("F")
s << "0" * precision
a, b = s.split(".", 2)
a << "."
a << b[0, precision]
end
else
formatted_string = rounded_number
end
delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
format_number(delimited_number)
end
private
def strip_insignificant_zeros
options[:strip_insignificant_zeros]
end
def format_number(number)
if strip_insignificant_zeros
escaped_separator = Regexp.escape(options[:separator])
number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, "")
else
number
end
end
end
end
end
# frozen_string_literal: true
module ActiveSupport
module NumberHelper
class RoundingHelper # :nodoc:
attr_reader :options
def initialize(options)
@options = options
end
def round(number)
precision = absolute_precision(number)
return number unless precision
rounded_number = convert_to_decimal(number).round(precision, options.fetch(:round_mode, :default))
rounded_number.zero? ? rounded_number.abs : rounded_number # prevent showing negative zeros
end
def digit_count(number)
return 1 if number.zero?
(Math.log10(number.abs) + 1).floor
end
private
def convert_to_decimal(number)
case number
when Float, String
BigDecimal(number.to_s)
when Rational
BigDecimal(number, digit_count(number.to_i) + options[:precision])
else
number.to_d
end
end
def absolute_precision(number)
if significant && options[:precision] > 0
options[:precision] - digit_count(convert_to_decimal(number))
else
options[:precision]
end
end
def significant
options[:significant]
end
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/hash/deep_merge"
- 2
require "active_support/core_ext/symbol/starts_ends_with"
- 2
module ActiveSupport
- 2
class OptionMerger #:nodoc:
- 2
instance_methods.each do |method|
- 152
undef_method(method) unless method.start_with?("__", "instance_eval", "class", "object_id")
end
- 2
def initialize(context, options)
@context, @options = context, options
end
- 2
private
- 2
def method_missing(method, *arguments, &block)
options = nil
if arguments.first.is_a?(Proc)
proc = arguments.pop
arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
elsif arguments.last.respond_to?(:to_hash)
options = @options.deep_merge(arguments.pop)
else
options = @options
end
invoke_method(method, arguments, options, &block)
end
- 2
if RUBY_VERSION >= "2.7"
def invoke_method(method, arguments, options, &block)
if options
@context.__send__(method, *arguments, **options, &block)
else
@context.__send__(method, *arguments, &block)
end
end
else
- 2
def invoke_method(method, arguments, options, &block)
arguments << options.dup if options
@context.__send__(method, *arguments, &block)
end
end
end
end
# frozen_string_literal: true
- 1
require "yaml"
- 1
YAML.add_builtin_type("omap") do |type, val|
ActiveSupport::OrderedHash[val.map { |v| v.to_a.first }]
end
- 1
module ActiveSupport
# DEPRECATED: <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves
# insertion order.
#
# oh = ActiveSupport::OrderedHash.new
# oh[:a] = 1
# oh[:b] = 2
# oh.keys # => [:a, :b], this order is guaranteed
#
# Also, maps the +omap+ feature for YAML files
# (See https://yaml.org/type/omap.html) to support ordered items
# when loading from yaml.
#
# <tt>ActiveSupport::OrderedHash</tt> is namespaced to prevent conflicts
# with other implementations.
- 1
class OrderedHash < ::Hash
- 1
def to_yaml_type
"!tag:yaml.org,2002:omap"
end
- 1
def encode_with(coder)
coder.represent_seq "!omap", map { |k, v| { k => v } }
end
- 1
def select(*args, &block)
dup.tap { |hash| hash.select!(*args, &block) }
end
- 1
def reject(*args, &block)
dup.tap { |hash| hash.reject!(*args, &block) }
end
- 1
def nested_under_indifferent_access
self
end
# Returns true to make sure that this hash is extractable via <tt>Array#extract_options!</tt>
- 1
def extractable_options?
true
end
end
end
# frozen_string_literal: true
- 2
require "active_support/core_ext/object/blank"
- 2
module ActiveSupport
# +OrderedOptions+ inherits from +Hash+ and provides dynamic accessor methods.
#
# With a +Hash+, key-value pairs are typically managed like this:
#
# h = {}
# h[:boy] = 'John'
# h[:girl] = 'Mary'
# h[:boy] # => 'John'
# h[:girl] # => 'Mary'
# h[:dog] # => nil
#
# Using +OrderedOptions+, the above code can be written as:
#
# h = ActiveSupport::OrderedOptions.new
# h.boy = 'John'
# h.girl = 'Mary'
# h.boy # => 'John'
# h.girl # => 'Mary'
# h.dog # => nil
#
# To raise an exception when the value is blank, append a
# bang to the key name, like:
#
# h.dog! # => raises KeyError: :dog is blank
#
- 2
class OrderedOptions < Hash
- 2
alias_method :_get, :[] # preserve the original #[] method
- 2
protected :_get # make it protected
- 2
def []=(key, value)
super(key.to_sym, value)
end
- 2
def [](key)
super(key.to_sym)
end
- 2
def method_missing(name, *args)
name_string = +name.to_s
if name_string.chomp!("=")
self[name_string] = args.first
else
bangs = name_string.chomp!("!")
if bangs
self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
else
self[name_string]
end
end
end
- 2
def respond_to_missing?(name, include_private)
true
end
- 2
def extractable_options?
true
end
end
# +InheritableOptions+ provides a constructor to build an +OrderedOptions+
# hash inherited from another hash.
#
# Use this if you already have some hash and you want to create a new one based on it.
#
# h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' })
# h.girl # => 'Mary'
# h.boy # => 'John'
- 2
class InheritableOptions < OrderedOptions
- 2
def initialize(parent = nil)
if parent.kind_of?(OrderedOptions)
# use the faster _get when dealing with OrderedOptions
super() { |h, k| parent._get(k) }
elsif parent
super() { |h, k| parent[k] }
else
super()
end
end
- 2
def inheritable_copy
self.class.new(self)
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/duplicable"
- 1
module ActiveSupport
# +ParameterFilter+ allows you to specify keys for sensitive data from
# hash-like object and replace corresponding value. Filtering only certain
# sub-keys from a hash is possible by using the dot notation:
# 'credit_card.number'. If a proc is given, each key and value of a hash and
# all sub-hashes are passed to it, where the value or the key can be replaced
# using String#replace or similar methods.
#
# ActiveSupport::ParameterFilter.new([:password])
# => replaces the value to all keys matching /password/i with "[FILTERED]"
#
# ActiveSupport::ParameterFilter.new([:foo, "bar"])
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
# ActiveSupport::ParameterFilter.new(["credit_card.code"])
# => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
# change { file: { code: "xxxx"} }
#
# ActiveSupport::ParameterFilter.new([-> (k, v) do
# v.reverse! if /secret/i.match?(k)
# end])
# => reverses the value to all keys matching /secret/i
- 1
class ParameterFilter
- 1
FILTERED = "[FILTERED]" # :nodoc:
# Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+.
# Other types of filters are treated as +String+ using +to_s+.
# For +Proc+ filters, key, value, and optional original hash is passed to block arguments.
#
# ==== Options
#
# * <tt>:mask</tt> - A replaced object when filtered. Defaults to +"[FILTERED]"+
- 1
def initialize(filters = [], mask: FILTERED)
@filters = filters
@mask = mask
end
# Mask value of +params+ if key matches one of filters.
- 1
def filter(params)
compiled_filter.call(params)
end
# Returns filtered value for given key. For +Proc+ filters, third block argument is not populated.
- 1
def filter_param(key, value)
@filters.empty? ? value : compiled_filter.value_for_key(key, value)
end
- 1
private
- 1
def compiled_filter
@compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask)
end
- 1
class CompiledFilter # :nodoc:
- 1
def self.compile(filters, mask:)
return lambda { |params| params.dup } if filters.empty?
strings, regexps, blocks, deep_regexps, deep_strings = [], [], [], nil, nil
filters.each do |item|
case item
when Proc
blocks << item
when Regexp
if item.to_s.include?("\\.")
(deep_regexps ||= []) << item
else
regexps << item
end
else
s = Regexp.escape(item.to_s)
if s.include?("\\.")
(deep_strings ||= []) << s
else
strings << s
end
end
end
regexps << Regexp.new(strings.join("|"), true) unless strings.empty?
(deep_regexps ||= []) << Regexp.new(deep_strings.join("|"), true) if deep_strings&.any?
new regexps, deep_regexps, blocks, mask: mask
end
- 1
attr_reader :regexps, :deep_regexps, :blocks
- 1
def initialize(regexps, deep_regexps, blocks, mask:)
@regexps = regexps
@deep_regexps = deep_regexps&.any? ? deep_regexps : nil
@blocks = blocks
@mask = mask
end
- 1
def call(params, parents = [], original_params = params)
filtered_params = params.class.new
params.each do |key, value|
filtered_params[key] = value_for_key(key, value, parents, original_params)
end
filtered_params
end
- 1
def value_for_key(key, value, parents = [], original_params = nil)
parents.push(key) if deep_regexps
if regexps.any? { |r| r.match?(key.to_s) }
value = @mask
elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) }
value = @mask
elsif value.is_a?(Hash)
value = call(value, parents, original_params)
elsif value.is_a?(Array)
# If we don't pop the current parent it will be duplicated as we
# process each array value.
parents.pop if deep_regexps
value = value.map { |v| value_for_key(key, v, parents, original_params) }
# Restore the parent stack after processing the array.
parents.push(key) if deep_regexps
elsif blocks.any?
key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) }
end
parents.pop if deep_regexps
value
end
end
end
end
# frozen_string_literal: true
- 24
require "active_support/core_ext/module/delegation"
- 24
module ActiveSupport
# NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends.
# Please use that approach instead.
#
# This module is used to encapsulate access to thread local variables.
#
# Instead of polluting the thread locals namespace:
#
# Thread.current[:connection_handler]
#
# you define a class that extends this module:
#
# module ActiveRecord
# class RuntimeRegistry
# extend ActiveSupport::PerThreadRegistry
#
# attr_accessor :connection_handler
# end
# end
#
# and invoke the declared instance accessors as class methods. So
#
# ActiveRecord::RuntimeRegistry.connection_handler = connection_handler
#
# sets a connection handler local to the current thread, and
#
# ActiveRecord::RuntimeRegistry.connection_handler
#
# returns a connection handler local to the current thread.
#
# This feature is accomplished by instantiating the class and storing the
# instance as a thread local keyed by the class name. In the example above
# a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>.
# The class methods proxy to said thread local instance.
#
# If the class has an initializer, it must accept no arguments.
- 24
module PerThreadRegistry
- 24
def self.extended(object)
- 29
object.instance_variable_set :@per_thread_registry_key, object.name.freeze
end
- 24
def instance
Thread.current[@per_thread_registry_key] ||= new
end
- 24
private
- 24
def method_missing(name, *args, &block)
# Caches the method definition as a singleton method of the receiver.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
singleton_class.delegate name, to: :instance
send(name, *args, &block)
end
end
end
# frozen_string_literal: true
module ActiveSupport
# A class with no predefined methods that behaves similarly to Builder's
# BlankSlate. Used for proxy classes.
class ProxyObject < ::BasicObject
undef_method :==
undef_method :equal?
# Let ActiveSupport::ProxyObject at least raise exceptions.
def raise(*args)
::Object.send(:raise, *args)
end
end
end
# frozen_string_literal: true
# This is a private interface.
#
# Rails components cherry pick from Active Support as needed, but there are a
# few features that are used for sure in some way or another and it is not worth
# putting individual requires absolutely everywhere. Think blank? for example.
#
# This file is loaded by every Rails component except Active Support itself,
# but it does not belong to the Rails public interface. It is internal to
# Rails and can change anytime.
# Defines Object#blank? and Object#present?.
require "active_support/core_ext/object/blank"
# Support for ClassMethods and the included macro.
require "active_support/concern"
# Defines Class#class_attribute.
require "active_support/core_ext/class/attribute"
# Defines Module#delegate.
require "active_support/core_ext/module/delegation"
# Defines ActiveSupport::Deprecation.
require "active_support/deprecation"
# frozen_string_literal: true
require "active_support"
require "active_support/i18n_railtie"
module ActiveSupport
class Railtie < Rails::Railtie # :nodoc:
config.active_support = ActiveSupport::OrderedOptions.new
config.eager_load_namespaces << ActiveSupport
initializer "active_support.set_authenticated_message_encryption" do |app|
config.after_initialize do
unless app.config.active_support.use_authenticated_message_encryption.nil?
ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
app.config.active_support.use_authenticated_message_encryption
end
end
end
initializer "active_support.reset_all_current_attributes_instances" do |app|
app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all }
app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all }
ActiveSupport.on_load(:active_support_test_case) do
require "active_support/current_attributes/test_helper"
include ActiveSupport::CurrentAttributes::TestHelper
end
end
initializer "active_support.deprecation_behavior" do |app|
if deprecation = app.config.active_support.deprecation
ActiveSupport::Deprecation.behavior = deprecation
end
if disallowed_deprecation = app.config.active_support.disallowed_deprecation
ActiveSupport::Deprecation.disallowed_behavior = disallowed_deprecation
end
if disallowed_warnings = app.config.active_support.disallowed_deprecation_warnings
ActiveSupport::Deprecation.disallowed_warnings = disallowed_warnings
end
end
# Sets the default value for Time.zone
# If assigned value cannot be matched to a TimeZone, an exception will be raised.
initializer "active_support.initialize_time_zone" do |app|
begin
TZInfo::DataSource.get
rescue TZInfo::DataSourceNotFound => e
raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
end
require "active_support/core_ext/time/zones"
Time.zone_default = Time.find_zone!(app.config.time_zone)
end
# Sets the default week start
# If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
initializer "active_support.initialize_beginning_of_week" do |app|
require "active_support/core_ext/date/calculations"
beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week)
Date.beginning_of_week_default = beginning_of_week_default
end
initializer "active_support.require_master_key" do |app|
if app.config.respond_to?(:require_master_key) && app.config.require_master_key
begin
app.credentials.key
rescue ActiveSupport::EncryptedFile::MissingKeyError => error
$stderr.puts error.message
exit 1
end
end
end
initializer "active_support.set_configs" do |app|
app.config.active_support.each do |k, v|
k = "#{k}="
ActiveSupport.send(k, v) if ActiveSupport.respond_to? k
end
end
initializer "active_support.set_hash_digest_class" do |app|
config.after_initialize do
if app.config.active_support.use_sha1_digests
ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1
end
end
end
end
end
# frozen_string_literal: true
require "active_support/execution_wrapper"
require "active_support/executor"
module ActiveSupport
#--
# This class defines several callbacks:
#
# to_prepare -- Run once at application startup, and also from
# +to_run+.
#
# to_run -- Run before a work run that is reloading. If
# +reload_classes_only_on_change+ is true (the default), the class
# unload will have already occurred.
#
# to_complete -- Run after a work run that has reloaded. If
# +reload_classes_only_on_change+ is false, the class unload will
# have occurred after the work run, but before this callback.
#
# before_class_unload -- Run immediately before the classes are
# unloaded.
#
# after_class_unload -- Run immediately after the classes are
# unloaded.
#
class Reloader < ExecutionWrapper
define_callbacks :prepare
define_callbacks :class_unload
# Registers a callback that will run once at application startup and every time the code is reloaded.
def self.to_prepare(*args, &block)
set_callback(:prepare, *args, &block)
end
# Registers a callback that will run immediately before the classes are unloaded.
def self.before_class_unload(*args, &block)
set_callback(:class_unload, *args, &block)
end
# Registers a callback that will run immediately after the classes are unloaded.
def self.after_class_unload(*args, &block)
set_callback(:class_unload, :after, *args, &block)
end
to_run(:after) { self.class.prepare! }
# Initiate a manual reload
def self.reload!
executor.wrap do
new.tap do |instance|
instance.run!
ensure
instance.complete!
end
end
prepare!
end
def self.run! # :nodoc:
if check!
super
else
Null
end
end
# Run the supplied block as a work unit, reloading code as needed
def self.wrap
executor.wrap do
super
end
end
class_attribute :executor, default: Executor
class_attribute :check, default: lambda { false }
def self.check! # :nodoc:
@should_reload ||= check.call
end
def self.reloaded! # :nodoc:
@should_reload = false
end
def self.prepare! # :nodoc:
new.run_callbacks(:prepare)
end
def initialize
super
@locked = false
end
# Acquire the ActiveSupport::Dependencies::Interlock unload lock,
# ensuring it will be released automatically
def require_unload_lock!
unless @locked
ActiveSupport::Dependencies.interlock.start_unloading
@locked = true
end
end
# Release the unload lock if it has been previously obtained
def release_unload_lock!
if @locked
@locked = false
ActiveSupport::Dependencies.interlock.done_unloading
end
end
def run! # :nodoc:
super
release_unload_lock!
end
def class_unload!(&block) # :nodoc:
require_unload_lock!
run_callbacks(:class_unload, &block)
end
def complete! # :nodoc:
super
self.class.reloaded!
ensure
release_unload_lock!
end
end
end
# frozen_string_literal: true
- 1
require "active_support/concern"
- 1
require "active_support/core_ext/class/attribute"
- 1
require "active_support/core_ext/string/inflections"
- 1
module ActiveSupport
# Rescuable module adds support for easier exception handling.
- 1
module Rescuable
- 1
extend Concern
- 1
included do
- 1
class_attribute :rescue_handlers, default: []
end
- 1
module ClassMethods
# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing <tt>:with</tt> option with the name of a method
# or a Proc object to be called to handle them. Alternatively a block can
# be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
# any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, with: :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, with: :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render xml: exception, status: 500
# end
#
# private
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
#
# Exceptions raised inside exception handlers are not propagated up.
- 1
def rescue_from(*klasses, with: nil, &block)
- 6
unless with
- 3
if block_given?
- 3
with = block
else
raise ArgumentError, "Need a handler. Pass the with: keyword argument or provide a block."
end
end
- 6
klasses.each do |klass|
- 6
key = if klass.is_a?(Module) && klass.respond_to?(:===)
- 5
klass.name
- 1
elsif klass.is_a?(String)
- 1
klass
else
raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class"
end
# Put the new handler at the end because the list is read in reverse.
- 6
self.rescue_handlers += [[key, with]]
end
end
# Matches an exception to a handler based on the exception class.
#
# If no handler matches the exception, check for a handler matching the
# (optional) exception.cause. If no handler matches the exception or its
# cause, this returns +nil+, so you can deal with unhandled exceptions.
# Be sure to re-raise unhandled exceptions if this is what you expect.
#
# begin
# …
# rescue => exception
# rescue_with_handler(exception) || raise
# end
#
# Returns the exception if it was handled and +nil+ if it was not.
- 1
def rescue_with_handler(exception, object: self, visited_exceptions: [])
visited_exceptions << exception
if handler = handler_for_rescue(exception, object: object)
handler.call exception
exception
elsif exception
if visited_exceptions.include?(exception.cause)
nil
else
rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
end
end
end
- 1
def handler_for_rescue(exception, object: self) #:nodoc:
case rescuer = find_rescue_handler(exception)
when Symbol
method = object.method(rescuer)
if method.arity == 0
-> e { method.call }
else
method
end
when Proc
if rescuer.arity == 0
-> e { object.instance_exec(&rescuer) }
else
-> e { object.instance_exec(e, &rescuer) }
end
end
end
- 1
private
- 1
def find_rescue_handler(exception)
if exception
# Handlers are in order of declaration but the most recently declared
# is the highest priority match, so we search for matching handlers
# in reverse.
_, handler = rescue_handlers.reverse_each.detect do |class_or_name, _|
if klass = constantize_rescue_handler_class(class_or_name)
klass === exception
end
end
handler
end
end
- 1
def constantize_rescue_handler_class(class_or_name)
case class_or_name
when String, Symbol
begin
# Try a lexical lookup first since we support
#
# class Super
# rescue_from 'Error', with: …
# end
#
# class Sub
# class Error < StandardError; end
# end
#
# so an Error raised in Sub will hit the 'Error' handler.
const_get class_or_name
rescue NameError
class_or_name.safe_constantize
end
else
class_or_name
end
end
end
# Delegates to the class method, but uses the instance as the subject for
# rescue_from handlers (method calls, instance_exec blocks).
- 1
def rescue_with_handler(exception)
self.class.rescue_with_handler exception, object: self
end
# Internal handler lookup. Delegates to class method. Some libraries call
# this directly, so keeping it around for compatibility.
- 1
def handler_for_rescue(exception) #:nodoc:
self.class.handler_for_rescue exception, object: self
end
end
end
# frozen_string_literal: true
- 1
require "active_support/security_utils"
- 1
require "active_support/messages/rotator"
- 1
module ActiveSupport
# The ActiveSupport::SecureCompareRotator is a wrapper around +ActiveSupport::SecurityUtils.secure_compare+
# and allows you to rotate a previously defined value to a new one.
#
# It can be used as follow:
#
# rotator = ActiveSupport::SecureCompareRotator.new('new_production_value')
# rotator.rotate('previous_production_value')
# rotator.secure_compare!('previous_production_value')
#
# One real use case example would be to rotate a basic auth credentials:
#
# class MyController < ApplicationController
# def authenticate_request
# rotator = ActiveSupport::SecureComparerotator.new('new_password')
# rotator.rotate('old_password')
#
# authenticate_or_request_with_http_basic do |username, password|
# rotator.secure_compare!(password)
# rescue ActiveSupport::SecureCompareRotator::InvalidMatch
# false
# end
# end
# end
- 1
class SecureCompareRotator
- 1
include SecurityUtils
- 1
prepend Messages::Rotator
- 1
InvalidMatch = Class.new(StandardError)
- 1
def initialize(value, **_options)
@value = value
end
- 1
def secure_compare!(other_value, on_rotation: @on_rotation)
secure_compare(@value, other_value) ||
run_rotations(on_rotation) { |wrapper| wrapper.secure_compare!(other_value) } ||
raise(InvalidMatch)
end
- 1
private
- 1
def build_rotation(previous_value, _options)
self.class.new(previous_value)
end
end
end
# frozen_string_literal: true
- 2
module ActiveSupport
- 2
module SecurityUtils
# Constant time string comparison, for fixed length strings.
#
# The values compared should be of fixed length, such as strings
# that have already been processed by HMAC. Raises in case of length mismatch.
- 2
def fixed_length_secure_compare(a, b)
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
- 2
module_function :fixed_length_secure_compare
# Secure string comparison for strings of variable length.
#
# While a timing attack would not be able to discern the content of
# a secret compared via secure_compare, it is possible to determine
# the secret length. This should be considered when using secure_compare
# to compare weak, short secrets to user input.
- 2
def secure_compare(a, b)
a.length == b.length && fixed_length_secure_compare(a, b)
end
- 2
module_function :secure_compare
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/symbol/starts_ends_with"
- 1
module ActiveSupport
# Wrapping a string in this class gives you a prettier way to test
# for equality. The value returned by <tt>Rails.env</tt> is wrapped
# in a StringInquirer object, so instead of calling this:
#
# Rails.env == 'production'
#
# you can call this:
#
# Rails.env.production?
#
# == Instantiating a new StringInquirer
#
# vehicle = ActiveSupport::StringInquirer.new('car')
# vehicle.car? # => true
# vehicle.bike? # => false
- 1
class StringInquirer < String
- 1
private
- 1
def respond_to_missing?(method_name, include_private = false)
method_name.end_with?("?") || super
end
- 1
def method_missing(method_name, *arguments)
if method_name.end_with?("?")
self == method_name[0..-2]
else
super
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/per_thread_registry"
- 1
require "active_support/notifications"
- 1
module ActiveSupport
# ActiveSupport::Subscriber is an object set to consume
# ActiveSupport::Notifications. The subscriber dispatches notifications to
# a registered object based on its given namespace.
#
# An example would be an Active Record subscriber responsible for collecting
# statistics about queries:
#
# module ActiveRecord
# class StatsSubscriber < ActiveSupport::Subscriber
# attach_to :active_record
#
# def sql(event)
# Statsd.timing("sql.#{event.payload[:name]}", event.duration)
# end
# end
# end
#
# After configured, whenever a "sql.active_record" notification is published,
# it will properly dispatch the event (ActiveSupport::Notifications::Event) to
# the +sql+ method.
#
# We can detach a subscriber as well:
#
# ActiveRecord::StatsSubscriber.detach_from(:active_record)
- 1
class Subscriber
- 1
class << self
# Attach the subscriber to a namespace.
- 1
def attach_to(namespace, subscriber = new, notifier = ActiveSupport::Notifications)
- 2
@namespace = namespace
- 2
@subscriber = subscriber
- 2
@notifier = notifier
- 2
subscribers << subscriber
# Add event subscribers for all existing methods on the class.
- 2
subscriber.public_methods(false).each do |event|
add_event_subscriber(event)
end
end
# Detach the subscriber from a namespace.
- 1
def detach_from(namespace, notifier = ActiveSupport::Notifications)
- 1
@namespace = namespace
- 1
@subscriber = find_attached_subscriber
- 1
@notifier = notifier
- 1
return unless subscriber
- 1
subscribers.delete(subscriber)
# Remove event subscribers of all existing methods on the class.
- 1
subscriber.public_methods(false).each do |event|
remove_event_subscriber(event)
end
# Reset notifier so that event subscribers will not add for new methods added to the class.
- 1
@notifier = nil
end
# Adds event subscribers for all new methods added to the class.
- 1
def method_added(event)
# Only public methods are added as subscribers, and only if a notifier
# has been set up. This means that subscribers will only be set up for
# classes that call #attach_to.
- 28
if public_method_defined?(event) && notifier
- 3
add_event_subscriber(event)
end
end
- 1
def subscribers
- 4
@@subscribers ||= []
end
- 1
private
- 1
attr_reader :subscriber, :notifier, :namespace
- 1
def add_event_subscriber(event) # :doc:
- 3
return if invalid_event?(event)
- 3
pattern = prepare_pattern(event)
# Don't add multiple subscribers (e.g. if methods are redefined).
- 3
return if pattern_subscribed?(pattern)
- 2
subscriber.patterns[pattern] = notifier.subscribe(pattern, subscriber)
end
- 1
def remove_event_subscriber(event) # :doc:
return if invalid_event?(event)
pattern = prepare_pattern(event)
return unless pattern_subscribed?(pattern)
notifier.unsubscribe(subscriber.patterns[pattern])
subscriber.patterns.delete(pattern)
end
- 1
def find_attached_subscriber
- 3
subscribers.find { |attached_subscriber| attached_subscriber.instance_of?(self) }
end
- 1
def invalid_event?(event)
- 3
%i{ start finish }.include?(event.to_sym)
end
- 1
def prepare_pattern(event)
- 3
"#{event}.#{namespace}"
end
- 1
def pattern_subscribed?(pattern)
- 3
subscriber.patterns.key?(pattern)
end
end
- 1
attr_reader :patterns # :nodoc:
- 1
def initialize
- 2
@queue_key = [self.class.name, object_id].join "-"
- 2
@patterns = {}
- 2
super
end
- 1
def start(name, id, payload)
event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload)
event.start!
parent = event_stack.last
parent << event if parent
event_stack.push event
end
- 1
def finish(name, id, payload)
event = event_stack.pop
event.finish!
event.payload.merge!(payload)
method = name.split(".").first
send(method, event)
end
- 1
private
- 1
def event_stack
SubscriberQueueRegistry.instance.get_queue(@queue_key)
end
end
# This is a registry for all the event stacks kept for subscribers.
#
# See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
# for further details.
- 1
class SubscriberQueueRegistry # :nodoc:
- 1
extend PerThreadRegistry
- 1
def initialize
@registry = {}
end
- 1
def get_queue(queue_key)
@registry[queue_key] ||= []
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/delegation"
- 1
require "active_support/core_ext/object/blank"
- 1
require "logger"
- 1
require "active_support/logger"
- 1
module ActiveSupport
# Wraps any standard Logger object to provide tagging capabilities.
#
# May be called with a block:
#
# logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
# logger.tagged('BCX') { logger.info 'Stuff' } # Logs "[BCX] Stuff"
# logger.tagged('BCX', "Jason") { logger.info 'Stuff' } # Logs "[BCX] [Jason] Stuff"
# logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff"
#
# If called without a block, a new logger will be returned with applied tags:
#
# logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
# logger.tagged("BCX").info "Stuff" # Logs "[BCX] Stuff"
# logger.tagged("BCX", "Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff"
# logger.tagged("BCX").tagged("Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff"
#
# This is used by the default Rails.logger as configured by Railties to make
# it easy to stamp log lines with subdomains, request ids, and anything else
# to aid debugging of multi-user production applications.
- 1
module TaggedLogging
- 1
module Formatter # :nodoc:
# This method is invoked when a log event occurs.
- 1
def call(severity, timestamp, progname, msg)
super(severity, timestamp, progname, "#{tags_text}#{msg}")
end
- 1
def tagged(*tags)
new_tags = push_tags(*tags)
yield self
ensure
pop_tags(new_tags.size)
end
- 1
def push_tags(*tags)
tags.flatten!
tags.reject!(&:blank?)
current_tags.concat tags
tags
end
- 1
def pop_tags(size = 1)
current_tags.pop size
end
- 1
def clear_tags!
current_tags.clear
end
- 1
def current_tags
# We use our object ID here to avoid conflicting with other instances
thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}"
Thread.current[thread_key] ||= []
end
- 1
def tags_text
tags = current_tags
if tags.one?
"[#{tags[0]}] "
elsif tags.any?
tags.collect { |tag| "[#{tag}] " }.join
end
end
end
- 1
module LocalTagStorage # :nodoc:
- 1
attr_accessor :current_tags
- 1
def self.extended(base)
base.current_tags = []
end
end
- 1
def self.new(logger)
logger = logger.dup
if logger.formatter
logger.formatter = logger.formatter.dup
else
# Ensure we set a default formatter so we aren't extending nil!
logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
end
logger.formatter.extend Formatter
logger.extend(self)
end
- 1
delegate :push_tags, :pop_tags, :clear_tags!, to: :formatter
- 1
def tagged(*tags)
if block_given?
formatter.tagged(*tags) { yield self }
else
logger = ActiveSupport::TaggedLogging.new(self)
logger.formatter.extend LocalTagStorage
logger.push_tags(*formatter.current_tags, *tags)
logger
end
end
- 1
def flush
clear_tags!
super if defined?(super)
end
end
end
# frozen_string_literal: true
- 23
gem "minitest" # make sure we get the gem, not stdlib
- 23
require "minitest"
- 23
require "active_support/testing/tagged_logging"
- 23
require "active_support/testing/setup_and_teardown"
- 23
require "active_support/testing/assertions"
- 23
require "active_support/testing/deprecation"
- 23
require "active_support/testing/declarative"
- 23
require "active_support/testing/isolation"
- 23
require "active_support/testing/constant_lookup"
- 23
require "active_support/testing/time_helpers"
- 23
require "active_support/testing/file_fixtures"
- 23
require "active_support/testing/parallelization"
- 23
require "concurrent/utility/processor_counter"
- 23
module ActiveSupport
- 23
class TestCase < ::Minitest::Test
- 23
Assertion = Minitest::Assertion
- 23
class << self
# Sets the order in which test cases are run.
#
# ActiveSupport::TestCase.test_order = :random # => :random
#
# Valid values are:
# * +:random+ (to run tests in random order)
# * +:parallel+ (to run tests in parallel)
# * +:sorted+ (to run tests alphabetically by method name)
# * +:alpha+ (equivalent to +:sorted+)
- 23
def test_order=(new_order)
ActiveSupport.test_order = new_order
end
# Returns the order in which test cases are run.
#
# ActiveSupport::TestCase.test_order # => :random
#
# Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
# Defaults to +:random+.
- 23
def test_order
- 898
ActiveSupport.test_order ||= :random
end
# Parallelizes the test suite.
#
# Takes a +workers+ argument that controls how many times the process
# is forked. For each process a new database will be created suffixed
# with the worker number.
#
# test-database-0
# test-database-1
#
# If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
# and the environment variable will be used instead. This is useful for CI
# environments, or other environments where you may need more workers than
# you do for local testing.
#
# If the number of workers is set to +1+ or fewer, the tests will not be
# parallelized.
#
# If +workers+ is set to +:number_of_processors+, the number of workers will be
# set to the actual core count on the machine you are on.
#
# The default parallelization method is to fork processes. If you'd like to
# use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
# method. Note the threaded parallelization does not create multiple
# database and will not work with system tests at this time.
#
# parallelize(workers: :number_of_processors, with: :threads)
#
# The threaded parallelization uses minitest's parallel executor directly.
# The processes parallelization uses a Ruby DRb server.
- 23
def parallelize(workers: :number_of_processors, with: :processes)
- 23
workers = Concurrent.physical_processor_count if workers == :number_of_processors
- 23
workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
- 23
return if workers <= 1
- 23
executor = case with
when :processes
- 23
Testing::Parallelization.new(workers)
when :threads
Minitest::Parallel::Executor.new(workers)
else
raise ArgumentError, "#{with} is not a supported parallelization executor."
end
- 23
self.lock_threads = false if defined?(self.lock_threads) && with == :threads
- 23
Minitest.parallel_executor = executor
- 23
parallelize_me!
end
# Set up hook for parallel testing. This can be used if you have multiple
# databases or any behavior that needs to be run after the process is forked
# but before the tests run.
#
# Note: this feature is not available with the threaded parallelization.
#
# In your +test_helper.rb+ add the following:
#
# class ActiveSupport::TestCase
# parallelize_setup do
# # create databases
# end
# end
- 23
def parallelize_setup(&block)
ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
yield worker
end
end
# Clean up hook for parallel testing. This can be used to drop databases
# if your app uses multiple write/read databases or other clean up before
# the tests finish. This runs before the forked process is closed.
#
# Note: this feature is not available with the threaded parallelization.
#
# In your +test_helper.rb+ add the following:
#
# class ActiveSupport::TestCase
# parallelize_teardown do
# # drop databases
# end
# end
- 23
def parallelize_teardown(&block)
ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
yield worker
end
end
end
- 23
alias_method :method_name, :name
- 23
include ActiveSupport::Testing::TaggedLogging
- 23
prepend ActiveSupport::Testing::SetupAndTeardown
- 23
include ActiveSupport::Testing::Assertions
- 23
include ActiveSupport::Testing::Deprecation
- 23
include ActiveSupport::Testing::TimeHelpers
- 23
include ActiveSupport::Testing::FileFixtures
- 23
extend ActiveSupport::Testing::Declarative
# test/unit backwards compatibility methods
- 23
alias :assert_raise :assert_raises
- 23
alias :assert_not_empty :refute_empty
- 23
alias :assert_not_equal :refute_equal
- 23
alias :assert_not_in_delta :refute_in_delta
- 23
alias :assert_not_in_epsilon :refute_in_epsilon
- 23
alias :assert_not_includes :refute_includes
- 23
alias :assert_not_instance_of :refute_instance_of
- 23
alias :assert_not_kind_of :refute_kind_of
- 23
alias :assert_no_match :refute_match
- 23
alias :assert_not_nil :refute_nil
- 23
alias :assert_not_operator :refute_operator
- 23
alias :assert_not_predicate :refute_predicate
- 23
alias :assert_not_respond_to :refute_respond_to
- 23
alias :assert_not_same :refute_same
- 23
ActiveSupport.run_load_hooks(:active_support_test_case, self)
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/enumerable"
- 23
module ActiveSupport
- 23
module Testing
- 23
module Assertions
- 23
UNTRACKED = Object.new # :nodoc:
# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
# like <tt>if foo</tt>.
#
# assert_not nil # => true
# assert_not false # => true
# assert_not 'foo' # => Expected "foo" to be nil or false
#
# An error message can be specified.
#
# assert_not foo, 'foo should be false'
- 23
def assert_not(object, message = nil)
message ||= "Expected #{mu_pp(object)} to be nil or false"
assert !object, message
end
# Assertion that the block should not raise an exception.
#
# Passes if evaluated code in the yielded block raises no exception.
#
# assert_nothing_raised do
# perform_service(param: 'no_exception')
# end
- 23
def assert_nothing_raised
yield
rescue => error
raise Minitest::UnexpectedError.new(error)
end
# Test numeric difference between the return value of an expression as a
# result of what is evaluated in the yielded block.
#
# assert_difference 'Article.count' do
# post :create, params: { article: {...} }
# end
#
# An arbitrary expression is passed in and evaluated.
#
# assert_difference 'Article.last.comments(:reload).size' do
# post :create, params: { comment: {...} }
# end
#
# An arbitrary positive or negative difference can be specified.
# The default is <tt>1</tt>.
#
# assert_difference 'Article.count', -1 do
# post :delete, params: { id: ... }
# end
#
# An array of expressions can also be passed in and evaluated.
#
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
# post :create, params: { article: {...} }
# end
#
# A hash of expressions/numeric differences can also be passed in and evaluated.
#
# assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
# post :create, params: { article: {...} }
# end
#
# A lambda or a list of lambdas can be passed in and evaluated:
#
# assert_difference ->{ Article.count }, 2 do
# post :create, params: { article: {...} }
# end
#
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
# post :create, params: { article: {...} }
# end
#
# An error message can be specified.
#
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
# post :delete, params: { id: ... }
# end
- 23
def assert_difference(expression, *args, &block)
expressions =
if expression.is_a?(Hash)
message = args[0]
expression
else
difference = args[0] || 1
message = args[1]
Array(expression).index_with(difference)
end
exps = expressions.keys.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
before = exps.map(&:call)
retval = assert_nothing_raised(&block)
expressions.zip(exps, before) do |(code, diff), exp, before_value|
error = "#{code.inspect} didn't change by #{diff}"
error = "#{message}.\n#{error}" if message
assert_equal(before_value + diff, exp.call, error)
end
retval
end
# Assertion that the numeric result of evaluating an expression is not
# changed before and after invoking the passed in block.
#
# assert_no_difference 'Article.count' do
# post :create, params: { article: invalid_attributes }
# end
#
# A lambda can be passed in and evaluated.
#
# assert_no_difference -> { Article.count } do
# post :create, params: { article: invalid_attributes }
# end
#
# An error message can be specified.
#
# assert_no_difference 'Article.count', 'An Article should not be created' do
# post :create, params: { article: invalid_attributes }
# end
#
# An array of expressions can also be passed in and evaluated.
#
# assert_no_difference [ 'Article.count', -> { Post.count } ] do
# post :create, params: { article: invalid_attributes }
# end
- 23
def assert_no_difference(expression, message = nil, &block)
assert_difference expression, 0, message, &block
end
# Assertion that the result of evaluating an expression is changed before
# and after invoking the passed in block.
#
# assert_changes 'Status.all_good?' do
# post :create, params: { status: { ok: false } }
# end
#
# You can pass the block as a string to be evaluated in the context of
# the block. A lambda can be passed for the block as well.
#
# assert_changes -> { Status.all_good? } do
# post :create, params: { status: { ok: false } }
# end
#
# The assertion is useful to test side effects. The passed block can be
# anything that can be converted to string with #to_s.
#
# assert_changes :@object do
# @object = 42
# end
#
# The keyword arguments :from and :to can be given to specify the
# expected initial value and the expected value after the block was
# executed.
#
# assert_changes :@object, from: nil, to: :foo do
# @object = :foo
# end
#
# An error message can be specified.
#
# assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
# post :create, params: { status: { incident: true } }
# end
- 23
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
before = exp.call
retval = assert_nothing_raised(&block)
unless from == UNTRACKED
error = "Expected change from #{from.inspect}"
error = "#{message}.\n#{error}" if message
assert from === before, error
end
after = exp.call
error = "#{expression.inspect} didn't change"
error = "#{error}. It was already #{to}" if before == to
error = "#{message}.\n#{error}" if message
assert_not_equal before, after, error
unless to == UNTRACKED
error = "Expected change to #{to}\n"
error = "#{message}.\n#{error}" if message
assert to === after, error
end
retval
end
# Assertion that the result of evaluating an expression is not changed before
# and after invoking the passed in block.
#
# assert_no_changes 'Status.all_good?' do
# post :create, params: { status: { ok: true } }
# end
#
# An error message can be specified.
#
# assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
# post :create, params: { status: { ok: false } }
# end
- 23
def assert_no_changes(expression, message = nil, &block)
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
before = exp.call
retval = assert_nothing_raised(&block)
after = exp.call
error = "#{expression.inspect} changed"
error = "#{message}.\n#{error}" if message
if before.nil?
assert_nil after, error
else
assert_equal before, after, error
end
retval
end
end
end
end
# frozen_string_literal: true
- 24
gem "minitest"
- 24
require "minitest"
- 24
Minitest.autorun
# frozen_string_literal: true
- 23
require "active_support/concern"
- 23
require "active_support/inflector"
- 23
module ActiveSupport
- 23
module Testing
# Resolves a constant from a minitest spec name.
#
# Given the following spec-style test:
#
# describe WidgetsController, :index do
# describe "authenticated user" do
# describe "returns widgets" do
# it "has a controller that exists" do
# assert_kind_of WidgetsController, @controller
# end
# end
# end
# end
#
# The test will have the following name:
#
# "WidgetsController::index::authenticated user::returns widgets"
#
# The constant WidgetsController can be resolved from the name.
# The following code will resolve the constant:
#
# controller = determine_constant_from_test_name(name) do |constant|
# Class === constant && constant < ::ActionController::Metal
# end
- 23
module ConstantLookup
- 23
extend ::ActiveSupport::Concern
- 23
module ClassMethods # :nodoc:
- 23
def determine_constant_from_test_name(test_name)
names = test_name.split "::"
while names.size > 0 do
names.last.sub!(/Test$/, "")
begin
constant = names.join("::").safe_constantize
break(constant) if yield(constant)
ensure
names.pop
end
end
end
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport
- 23
module Testing
- 23
module Declarative
- 23
unless defined?(Spec)
# Helper to define a test method using a String. Under the hood, it replaces
# spaces with underscores and defines the test method.
#
# test "verify something" do
# ...
# end
- 23
def test(name, &block)
- 526
test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
- 526
defined = method_defined? test_name
- 526
raise "#{test_name} is already defined in #{self}" if defined
- 526
if block_given?
- 526
define_method(test_name, &block)
else
define_method(test_name) do
flunk "No implementation provided for #{name}"
end
end
end
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/deprecation"
- 23
module ActiveSupport
- 23
module Testing
- 23
module Deprecation #:nodoc:
- 23
def assert_deprecated(match = nil, deprecator = nil, &block)
result, warnings = collect_deprecations(deprecator, &block)
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
if match
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
end
result
end
- 23
def assert_not_deprecated(deprecator = nil, &block)
result, deprecations = collect_deprecations(deprecator, &block)
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
result
end
- 23
def collect_deprecations(deprecator = nil)
deprecator ||= ActiveSupport::Deprecation
old_behavior = deprecator.behavior
deprecations = []
deprecator.behavior = Proc.new do |message, callstack|
deprecations << message
end
result = yield
[result, deprecations]
ensure
deprecator.behavior = old_behavior
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/concern"
- 23
module ActiveSupport
- 23
module Testing
# Adds simple access to sample files called file fixtures.
# File fixtures are normal files stored in
# <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
#
# File fixtures are represented as +Pathname+ objects.
# This makes it easy to extract specific information:
#
# file_fixture("example.txt").read # get the file's content
# file_fixture("example.mp3").size # get the file size
- 23
module FileFixtures
- 23
extend ActiveSupport::Concern
- 23
included do
- 23
class_attribute :file_fixture_path, instance_writer: false
end
# Returns a +Pathname+ to the fixture file named +fixture_name+.
#
# Raises +ArgumentError+ if +fixture_name+ can't be found.
- 23
def file_fixture(fixture_name)
path = Pathname.new(File.join(file_fixture_path, fixture_name))
if path.exist?
path
else
msg = "the directory '%s' does not contain a file named '%s'"
raise ArgumentError, msg % [file_fixture_path, fixture_name]
end
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport
- 23
module Testing
- 23
module Isolation
- 23
require "thread"
- 23
def self.included(klass) #:nodoc:
- 4
klass.class_eval do
- 4
parallelize_me!
end
end
- 23
def self.forking_env?
- 23
!ENV["NO_FORK"] && Process.respond_to?(:fork)
end
- 23
def run
serialized = run_in_isolation do
super
end
Marshal.load(serialized)
end
- 23
module Forking
- 23
def run_in_isolation(&blk)
read, write = IO.pipe
read.binmode
write.binmode
pid = fork do
read.close
yield
begin
if error?
failures.map! { |e|
begin
Marshal.dump e
e
rescue TypeError
ex = Exception.new e.message
ex.set_backtrace e.backtrace
Minitest::UnexpectedError.new ex
end
}
end
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
result = Marshal.dump(test_result)
end
write.puts [result].pack("m")
exit!
end
write.close
result = read.read
Process.wait2(pid)
result.unpack1("m")
end
end
- 23
module Subprocess
- 23
ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
# Crazy H4X to get this working in windows / jruby with
# no forking.
- 23
def run_in_isolation(&blk)
require "tempfile"
if ENV["ISOLATION_TEST"]
yield
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
file.puts [Marshal.dump(test_result)].pack("m")
end
exit!
else
Tempfile.open("isolation") do |tmpfile|
env = {
"ISOLATION_TEST" => self.class.name,
"ISOLATION_OUTPUT" => tmpfile.path
}
test_opts = "-n#{self.class.name}##{name}"
load_path_args = []
$-I.each do |p|
load_path_args << "-I"
load_path_args << File.expand_path(p)
end
child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
begin
Process.wait(child.pid)
rescue Errno::ECHILD # The child process may exit before we wait
nil
end
return tmpfile.read.unpack1("m")
end
end
end
end
- 23
include forking_env? ? Forking : Subprocess
end
end
end
# frozen_string_literal: true
- 24
require "minitest/mock"
- 24
module ActiveSupport
- 24
module Testing
- 24
module MethodCallAssertions # :nodoc:
- 24
private
- 24
def assert_called(object, method_name, message = nil, times: 1, returns: nil)
times_called = 0
object.stub(method_name, proc { times_called += 1; returns }) { yield }
error = "Expected #{method_name} to be called #{times} times, " \
"but was called #{times_called} times"
error = "#{message}.\n#{error}" if message
assert_equal times, times_called, error
end
- 24
def assert_called_with(object, method_name, args, returns: nil)
mock = Minitest::Mock.new
if args.all? { |arg| arg.is_a?(Array) }
args.each { |arg| mock.expect(:call, returns, arg) }
else
mock.expect(:call, returns, args)
end
object.stub(method_name, mock) { yield }
mock.verify
end
- 24
def assert_not_called(object, method_name, message = nil, &block)
assert_called(object, method_name, message, times: 0, &block)
end
- 24
def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)
times_called = 0
klass.define_method("stubbed_#{method_name}") do |*|
times_called += 1
returns
end
klass.alias_method "original_#{method_name}", method_name
klass.alias_method method_name, "stubbed_#{method_name}"
yield
error = "Expected #{method_name} to be called #{times} times, but was called #{times_called} times"
error = "#{message}.\n#{error}" if message
assert_equal times, times_called, error
ensure
klass.alias_method method_name, "original_#{method_name}"
klass.undef_method "original_#{method_name}"
klass.undef_method "stubbed_#{method_name}"
end
- 24
def assert_not_called_on_instance_of(klass, method_name, message = nil, &block)
assert_called_on_instance_of(klass, method_name, message, times: 0, &block)
end
- 24
def stub_any_instance(klass, instance: klass.new)
klass.stub(:new, instance) { yield instance }
end
end
end
end
# frozen_string_literal: true
- 23
require "drb"
- 23
require "drb/unix" unless Gem.win_platform?
- 23
require "active_support/core_ext/module/attribute_accessors"
- 23
require "active_support/testing/parallelization/server"
- 23
require "active_support/testing/parallelization/worker"
- 23
module ActiveSupport
- 23
module Testing
- 23
class Parallelization # :nodoc:
- 23
@@after_fork_hooks = []
- 23
def self.after_fork_hook(&blk)
@@after_fork_hooks << blk
end
- 23
cattr_reader :after_fork_hooks
- 23
@@run_cleanup_hooks = []
- 23
def self.run_cleanup_hook(&blk)
@@run_cleanup_hooks << blk
end
- 23
cattr_reader :run_cleanup_hooks
- 23
def initialize(worker_count)
- 23
@worker_count = worker_count
- 23
@queue_server = Server.new
- 23
@worker_pool = []
- 23
@url = DRb.start_service("drbunix:", @queue_server).uri
end
- 23
def start
- 23
@worker_pool = @worker_count.times.map do |worker|
- 46
Worker.new(worker, @url).start
end
end
- 23
def <<(work)
- 5804
@queue_server << work
end
- 23
def shutdown
- 23
@queue_server.shutdown
- 69
@worker_pool.each { |pid| Process.waitpid pid }
end
end
end
end
# frozen_string_literal: true
- 23
require "drb"
- 23
require "drb/unix" unless Gem.win_platform?
- 23
module ActiveSupport
- 23
module Testing
- 23
class Parallelization # :nodoc:
- 23
class Server
- 23
include DRb::DRbUndumped
- 23
def initialize
- 23
@queue = Queue.new
- 23
@active_workers = Concurrent::Map.new
- 23
@in_flight = Concurrent::Map.new
end
- 23
def record(reporter, result)
- 5804
raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown)
- 5804
@in_flight.delete([result.klass, result.name])
- 5804
reporter.synchronize do
- 5804
reporter.record(result)
end
end
- 23
def <<(o)
- 5804
o[2] = DRbObject.new(o[2]) if o
- 5804
@queue << o
end
- 23
def pop
- 5850
if test = @queue.pop
- 5804
@in_flight[[test[0].to_s, test[1]]] = test
- 5804
test
end
end
- 23
def start_worker(worker_id)
- 46
@active_workers[worker_id] = true
end
- 23
def stop_worker(worker_id)
- 46
@active_workers.delete(worker_id)
end
- 23
def active_workers?
- 91
@active_workers.size > 0
end
- 23
def shutdown
# Wait for initial queue to drain
- 23
while @queue.length != 0
- 885
sleep 0.1
end
- 23
@queue.close
# Wait until all workers have finished
- 23
while active_workers?
- 68
sleep 0.1
end
- 23
@in_flight.values.each do |(klass, name, reporter)|
result = Minitest::Result.from(klass.new(name))
error = RuntimeError.new("result not reported")
error.set_backtrace([""])
result.failures << Minitest::UnexpectedError.new(error)
reporter.synchronize do
reporter.record(result)
end
end
end
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport
- 23
module Testing
- 23
class Parallelization # :nodoc:
- 23
class Worker
- 23
def initialize(number, url)
- 46
@id = SecureRandom.uuid
- 46
@number = number
- 46
@url = url
- 46
@setup_exception = nil
end
- 23
def start
- 46
fork do
set_process_title("(starting)")
DRb.stop_service
@queue = DRbObject.new_with_uri(@url)
@queue.start_worker(@id)
begin
after_fork
rescue => @setup_exception; end
work_from_queue
ensure
set_process_title("(stopping)")
run_cleanup
@queue.stop_worker(@id)
end
end
- 23
def work_from_queue
while job = @queue.pop
perform_job(job)
end
end
- 23
def perform_job(job)
klass = job[0]
method = job[1]
reporter = job[2]
set_process_title("#{klass}##{method}")
result = klass.with_info_handler reporter do
Minitest.run_one_method(klass, method)
end
safe_record(reporter, result)
end
- 23
def safe_record(reporter, result)
add_setup_exception(result) if @setup_exception
begin
@queue.record(reporter, result)
rescue DRb::DRbConnError
result.failures.map! do |failure|
if failure.respond_to?(:error)
# minitest >5.14.0
error = DRb::DRbRemoteError.new(failure.error)
else
error = DRb::DRbRemoteError.new(failure.exception)
end
Minitest::UnexpectedError.new(error)
end
@queue.record(reporter, result)
end
set_process_title("(idle)")
end
- 23
def after_fork
Parallelization.after_fork_hooks.each do |cb|
cb.call(@number)
end
end
- 23
def run_cleanup
Parallelization.run_cleanup_hooks.each do |cb|
cb.call(@number)
end
end
- 23
private
- 23
def add_setup_exception(result)
result.failures.prepend Minitest::UnexpectedError.new(@setup_exception)
end
- 23
def set_process_title(status)
Process.setproctitle("Rails test worker #{@number} - #{status}")
end
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/callbacks"
- 23
module ActiveSupport
- 23
module Testing
# Adds support for +setup+ and +teardown+ callbacks.
# These callbacks serve as a replacement to overwriting the
# <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
#
# class ExampleTest < ActiveSupport::TestCase
# setup do
# # ...
# end
#
# teardown do
# # ...
# end
# end
- 23
module SetupAndTeardown
- 23
def self.prepended(klass)
- 23
klass.include ActiveSupport::Callbacks
- 23
klass.define_callbacks :setup, :teardown
- 23
klass.extend ClassMethods
end
- 23
module ClassMethods
# Add a callback, which runs before <tt>TestCase#setup</tt>.
- 23
def setup(*args, &block)
- 25
set_callback(:setup, :before, *args, &block)
end
# Add a callback, which runs after <tt>TestCase#teardown</tt>.
- 23
def teardown(*args, &block)
- 14
set_callback(:teardown, :after, *args, &block)
end
end
- 23
def before_setup # :nodoc:
super
run_callbacks :setup
end
- 23
def after_teardown # :nodoc:
begin
run_callbacks :teardown
rescue => e
self.failures << Minitest::UnexpectedError.new(e)
end
super
end
end
end
end
# frozen_string_literal: true
- 1
module ActiveSupport
- 1
module Testing
- 1
module Stream #:nodoc:
- 1
private
- 1
def silence_stream(stream)
old_stream = stream.dup
stream.reopen(IO::NULL)
stream.sync = true
yield
ensure
stream.reopen(old_stream)
old_stream.close
end
- 1
def quietly
silence_stream(STDOUT) do
silence_stream(STDERR) do
yield
end
end
end
- 1
def capture(stream)
stream = stream.to_s
captured_stream = Tempfile.new(stream)
stream_io = eval("$#{stream}")
origin_stream = stream_io.dup
stream_io.reopen(captured_stream)
yield
stream_io.rewind
captured_stream.read
ensure
captured_stream.close
captured_stream.unlink
stream_io.reopen(origin_stream)
end
end
end
end
# frozen_string_literal: true
- 23
module ActiveSupport
- 23
module Testing
# Logs a "PostsControllerTest: test name" heading before each test to
# make test.log easier to search and follow along with.
- 23
module TaggedLogging #:nodoc:
- 23
attr_writer :tagged_logger
- 23
def before_setup
if tagged_logger && tagged_logger.info?
heading = "#{self.class}: #{name}"
divider = "-" * heading.size
tagged_logger.info divider
tagged_logger.info heading
tagged_logger.info divider
end
super
end
- 23
private
- 23
def tagged_logger
@tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/module/redefine_method"
- 23
require "active_support/core_ext/time/calculations"
- 23
require "concurrent/map"
- 23
module ActiveSupport
- 23
module Testing
- 23
class SimpleStubs # :nodoc:
- 23
Stub = Struct.new(:object, :method_name, :original_method)
- 23
def initialize
@stubs = Concurrent::Map.new { |h, k| h[k] = {} }
end
- 23
def stub_object(object, method_name, &block)
if stub = stubbing(object, method_name)
unstub_object(stub)
end
new_name = "__simple_stub__#{method_name}"
@stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
object.singleton_class.alias_method new_name, method_name
object.define_singleton_method(method_name, &block)
end
- 23
def unstub_all!
@stubs.each_value do |object_stubs|
object_stubs.each_value do |stub|
unstub_object(stub)
end
end
@stubs.clear
end
- 23
def stubbing(object, method_name)
@stubs[object.object_id][method_name]
end
- 23
def stubbed?
!@stubs.empty?
end
- 23
private
- 23
def unstub_object(stub)
singleton_class = stub.object.singleton_class
singleton_class.silence_redefinition_of_method stub.method_name
singleton_class.alias_method stub.method_name, stub.original_method
singleton_class.undef_method stub.original_method
end
end
# Contains helpers that help you test passage of time.
- 23
module TimeHelpers
- 23
def after_teardown
travel_back
super
end
# Changes current time to the time in the future or in the past by a given time difference by
# stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
# at the end of the test.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# travel 1.day
# Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
# Date.current # => Sun, 10 Nov 2013
# DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500
#
# This method also accepts a block, which will return the current time back to its original
# state at the end of the block:
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# travel 1.day do
# User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
# end
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
- 23
def travel(duration, &block)
travel_to Time.now + duration, &block
end
# Changes current time to the given time by stubbing +Time.now+,
# +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
# The stubs are automatically removed at the end of the test.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
# Date.current # => Wed, 24 Nov 2004
# DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
#
# Dates are taken as their timestamp at the beginning of the day in the
# application time zone. <tt>Time.current</tt> returns said timestamp,
# and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
# <tt>Date.current</tt> returns a date equal to the argument, and
# <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
# be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
# or <tt>Date.today</tt>, in order to honor the application time zone
# please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
#
# Note that the usec for the time passed will be set to 0 to prevent rounding
# errors with external services, like MySQL (which will round instead of floor,
# leading to off-by-one-second errors).
#
# This method also accepts a block, which will return the current time back to its original
# state at the end of the block:
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44) do
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
# end
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
- 23
def travel_to(date_or_time)
if block_given? && simple_stubs.stubbing(Time, :now)
travel_to_nested_block_call = <<~MSG
Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
Instead of:
travel_to 2.days.from_now do
# 2 days from today
travel_to 3.days.from_now do
# 5 days from today
end
end
preferred way to achieve above is:
travel 2.days do
# 2 days from today
end
travel 5.days do
# 5 days from today
end
MSG
raise travel_to_nested_block_call
end
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
now = date_or_time.midnight.to_time
else
now = date_or_time.to_time.change(usec: 0)
end
simple_stubs.stub_object(Time, :now) { at(now.to_i) }
simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
if block_given?
begin
yield
ensure
travel_back
end
end
end
# Returns the current time back to its original state, by removing the stubs added by
# +travel+, +travel_to+, and +freeze_time+.
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
#
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
#
# travel_back
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
#
# This method also accepts a block, which brings the stubs back at the end of the block:
#
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
#
# travel_to Time.zone.local(2004, 11, 24, 1, 4, 44)
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
#
# travel_back do
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
# end
#
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
- 23
def travel_back
stubbed_time = Time.current if block_given? && simple_stubs.stubbed?
simple_stubs.unstub_all!
yield if block_given?
ensure
travel_to stubbed_time if stubbed_time
end
- 23
alias_method :unfreeze_time, :travel_back
# Calls +travel_to+ with +Time.now+.
#
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
# freeze_time
# sleep(1)
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
#
# This method also accepts a block, which will return the current time back to its original
# state at the end of the block:
#
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
# freeze_time do
# sleep(1)
# User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
# end
# Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
- 23
def freeze_time(&block)
travel_to Time.now, &block
end
- 23
private
- 23
def simple_stubs
@simple_stubs ||= SimpleStubs.new
end
end
end
end
# frozen_string_literal: true
- 2
module ActiveSupport
- 2
autoload :Duration, "active_support/duration"
- 2
autoload :TimeWithZone, "active_support/time_with_zone"
- 2
autoload :TimeZone, "active_support/values/time_zone"
end
- 2
require "date"
- 2
require "time"
- 2
require "active_support/core_ext/time"
- 2
require "active_support/core_ext/date"
- 2
require "active_support/core_ext/date_time"
- 2
require "active_support/core_ext/integer/time"
- 2
require "active_support/core_ext/numeric/time"
- 2
require "active_support/core_ext/string/conversions"
- 2
require "active_support/core_ext/string/zones"
# frozen_string_literal: true
- 23
require "active_support/duration"
- 23
require "active_support/values/time_zone"
- 23
require "active_support/core_ext/object/acts_like"
- 23
require "active_support/core_ext/date_and_time/compatibility"
- 23
module ActiveSupport
# A Time-like class that can represent a time in any time zone. Necessary
# because standard Ruby Time instances are limited to UTC and the
# system's <tt>ENV['TZ']</tt> zone.
#
# You shouldn't ever need to create a TimeWithZone instance directly via +new+.
# Instead use methods +local+, +parse+, +at+ and +now+ on TimeZone instances,
# and +in_time_zone+ on Time and DateTime instances.
#
# 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.000000000 EST -05:00
# Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
# Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
# Time.zone.now # => Sun, 18 May 2008 13:07:55.754107581 EDT -04:00
# Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45.000000000 EST -05:00
#
# See Time and TimeZone for further documentation of these methods.
#
# TimeWithZone instances implement the same API as Ruby Time instances, so
# that Time and TimeWithZone instances are interchangeable.
#
# t = Time.zone.now # => Sun, 18 May 2008 13:27:25.031505668 EDT -04:00
# t.hour # => 13
# t.dst? # => true
# t.utc_offset # => -14400
# t.zone # => "EDT"
# t.to_s(:rfc822) # => "Sun, 18 May 2008 13:27:25 -0400"
# t + 1.day # => Mon, 19 May 2008 13:27:25.031505668 EDT -04:00
# t.beginning_of_year # => Tue, 01 Jan 2008 00:00:00.000000000 EST -05:00
# t > Time.utc(1999) # => true
# t.is_a?(Time) # => true
# t.is_a?(ActiveSupport::TimeWithZone) # => true
- 23
class TimeWithZone
# Report class name as 'Time' to thwart type checking.
- 23
def self.name
"Time"
end
- 23
PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" }
- 23
PRECISIONS[0] = "%FT%T"
- 23
include Comparable, DateAndTime::Compatibility
- 23
attr_reader :time_zone
- 23
def initialize(utc_time, time_zone, local_time = nil, period = nil)
@utc = utc_time ? transfer_time_values_to_utc_constructor(utc_time) : nil
@time_zone, @time = time_zone, local_time
@period = @utc ? period : get_period_and_ensure_valid_local_time(period)
end
# Returns a <tt>Time</tt> instance that represents the time in +time_zone+.
- 23
def time
@time ||= incorporate_utc_offset(@utc, utc_offset)
end
# Returns a <tt>Time</tt> instance of the simultaneous time in the UTC timezone.
- 23
def utc
@utc ||= incorporate_utc_offset(@time, -utc_offset)
end
- 23
alias_method :comparable_time, :utc
- 23
alias_method :getgm, :utc
- 23
alias_method :getutc, :utc
- 23
alias_method :gmtime, :utc
# Returns the underlying TZInfo::TimezonePeriod.
- 23
def period
@period ||= time_zone.period_for_utc(@utc)
end
# Returns the simultaneous time in <tt>Time.zone</tt>, or the specified zone.
- 23
def in_time_zone(new_zone = ::Time.zone)
return self if time_zone == new_zone
utc.in_time_zone(new_zone)
end
# Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
- 23
def localtime(utc_offset = nil)
utc.getlocal(utc_offset)
end
- 23
alias_method :getlocal, :localtime
# Returns true if the current time is within Daylight Savings Time for the
# specified time zone.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.parse("2012-5-30").dst? # => true
# Time.zone.parse("2012-11-30").dst? # => false
- 23
def dst?
period.dst?
end
- 23
alias_method :isdst, :dst?
# Returns true if the current time zone is set to UTC.
#
# Time.zone = 'UTC' # => 'UTC'
# Time.zone.now.utc? # => true
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.now.utc? # => false
- 23
def utc?
zone == "UTC" || zone == "UCT"
end
- 23
alias_method :gmt?, :utc?
# Returns the offset from current time to UTC time in seconds.
- 23
def utc_offset
period.observed_utc_offset
end
- 23
alias_method :gmt_offset, :utc_offset
- 23
alias_method :gmtoff, :utc_offset
# Returns a formatted string of the offset from UTC, or an alternative
# string if the time zone is already UTC.
#
# Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
# Time.zone.now.formatted_offset(true) # => "-05:00"
# Time.zone.now.formatted_offset(false) # => "-0500"
# Time.zone = 'UTC' # => "UTC"
# Time.zone.now.formatted_offset(true, "0") # => "0"
- 23
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
# Returns the time zone abbreviation.
#
# Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
# Time.zone.now.zone # => "EST"
- 23
def zone
period.abbreviation
end
# Returns a string of the object's date, time, zone, and offset from UTC.
#
# Time.zone.now.inspect # => "Thu, 04 Dec 2014 11:00:25.624541392 EST -05:00"
- 23
def inspect
"#{time.strftime('%a, %d %b %Y %H:%M:%S.%9N')} #{zone} #{formatted_offset}"
end
# Returns a string of the object's date and time in the ISO 8601 standard
# format.
#
# Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
- 23
def xmlschema(fraction_digits = 0)
"#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}"
end
- 23
alias_method :iso8601, :xmlschema
- 23
alias_method :rfc3339, :xmlschema
# Coerces time to a string for JSON encoding. The default format is ISO 8601.
# You can get %Y/%m/%d %H:%M:%S +offset style by setting
# <tt>ActiveSupport::JSON::Encoding.use_standard_json_time_format</tt>
# to +false+.
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
# Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
# # => "2005-02-01T05:15:10.000-10:00"
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
# Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
# # => "2005/02/01 05:15:10 -1000"
- 23
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
end
- 23
def init_with(coder) #:nodoc:
initialize(coder["utc"], coder["zone"], coder["time"])
end
- 23
def encode_with(coder) #:nodoc:
coder.tag = "!ruby/object:ActiveSupport::TimeWithZone"
coder.map = { "utc" => utc, "zone" => time_zone, "time" => time }
end
# Returns a string of the object's date and time in the format used by
# HTTP requests.
#
# Time.zone.now.httpdate # => "Tue, 01 Jan 2013 04:39:43 GMT"
- 23
def httpdate
utc.httpdate
end
# Returns a string of the object's date and time in the RFC 2822 standard
# format.
#
# Time.zone.now.rfc2822 # => "Tue, 01 Jan 2013 04:51:39 +0000"
- 23
def rfc2822
to_s(:rfc822)
end
- 23
alias_method :rfc822, :rfc2822
# Returns a string of the object's date and time.
# Accepts an optional <tt>format</tt>:
# * <tt>:default</tt> - default value, mimics Ruby Time#to_s format.
# * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
# * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
- 23
def to_s(format = :default)
if format == :db
utc.to_s(format)
elsif formatter = ::Time::DATE_FORMATS[format]
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
else
"#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format
end
end
- 23
alias_method :to_formatted_s, :to_s
# Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime,
# so that zone information is correct.
- 23
def strftime(format)
format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}")
getlocal(utc_offset).strftime(format)
end
# Use the time in UTC for comparisons.
- 23
def <=>(other)
utc <=> other
end
- 23
alias_method :before?, :<
- 23
alias_method :after?, :>
# Returns true if the current object's time is within the specified
# +min+ and +max+ time.
- 23
def between?(min, max)
utc.between?(min, max)
end
# Returns true if the current object's time is in the past.
- 23
def past?
utc.past?
end
# Returns true if the current object's time falls within
# the current day.
- 23
def today?
time.today?
end
# Returns true if the current object's time falls within
# the next day (tomorrow).
- 23
def tomorrow?
time.tomorrow?
end
- 23
alias :next_day? :tomorrow?
# Returns true if the current object's time falls within
# the previous day (yesterday).
- 23
def yesterday?
time.yesterday?
end
- 23
alias :prev_day? :yesterday?
# Returns true if the current object's time is in the future.
- 23
def future?
utc.future?
end
# Returns +true+ if +other+ is equal to current object.
- 23
def eql?(other)
other.eql?(utc)
end
- 23
def hash
utc.hash
end
# Adds an interval of time to the current object's time and returns that
# value as a new TimeWithZone object.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
# now + 1000 # => Sun, 02 Nov 2014 01:43:08.725182881 EDT -04:00
#
# If we're adding a Duration of variable length (i.e., years, months, days),
# move forward from #time, otherwise move forward from #utc, for accuracy
# when moving across DST boundaries.
#
# For instance, a time + 24.hours will advance exactly 24 hours, while a
# time + 1.day will advance 23-25 hours, depending on the day.
#
# now + 24.hours # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
# now + 1.day # => Mon, 03 Nov 2014 01:26:28.725182881 EST -05:00
- 23
def +(other)
if duration_of_variable_length?(other)
method_missing(:+, other)
else
result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
result.in_time_zone(time_zone)
end
end
- 23
alias_method :since, :+
- 23
alias_method :in, :+
# Subtracts an interval of time and returns a new TimeWithZone object unless
# the other value `acts_like?` time. Then it will return a Float of the difference
# between the two times that represents the difference between the current
# object's time and the +other+ time.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
# now - 1000 # => Mon, 03 Nov 2014 00:09:48.725182881 EST -05:00
#
# If subtracting a Duration of variable length (i.e., years, months, days),
# move backward from #time, otherwise move backward from #utc, for accuracy
# when moving across DST boundaries.
#
# For instance, a time - 24.hours will go subtract exactly 24 hours, while a
# time - 1.day will subtract 23-25 hours, depending on the day.
#
# now - 24.hours # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
# now - 1.day # => Sun, 02 Nov 2014 00:26:28.725182881 EDT -04:00
#
# If both the TimeWithZone object and the other value act like Time, a Float
# will be returned.
#
# Time.zone.now - 1.day.ago # => 86399.999967
#
- 23
def -(other)
if other.acts_like?(:time)
to_time - other.to_time
elsif duration_of_variable_length?(other)
method_missing(:-, other)
else
result = utc.acts_like?(:date) ? utc.ago(other) : utc - other rescue utc.ago(other)
result.in_time_zone(time_zone)
end
end
# Subtracts an interval of time from the current object's time and returns
# the result as a new TimeWithZone object.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28.725182881 EST -05:00
# now.ago(1000) # => Mon, 03 Nov 2014 00:09:48.725182881 EST -05:00
#
# If we're subtracting a Duration of variable length (i.e., years, months,
# days), move backward from #time, otherwise move backward from #utc, for
# accuracy when moving across DST boundaries.
#
# For instance, <tt>time.ago(24.hours)</tt> will move back exactly 24 hours,
# while <tt>time.ago(1.day)</tt> will move back 23-25 hours, depending on
# the day.
#
# now.ago(24.hours) # => Sun, 02 Nov 2014 01:26:28.725182881 EDT -04:00
# now.ago(1.day) # => Sun, 02 Nov 2014 00:26:28.725182881 EDT -04:00
- 23
def ago(other)
since(-other)
end
# Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have
# been changed according to the +options+ parameter. The time options (<tt>:hour</tt>,
# <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly,
# so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the
# hour and minute is passed, then sec, usec and nsec is set to 0. The +options+
# parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>,
# <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>,
# <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt>
# or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or
# <tt>:offset</tt>, not both.
#
# t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15.116992711 EST -05:00
# t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15.116992711 EST -05:00
# t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00.116992711 EST -05:00
# t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00.116992711 EST -05:00
# t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15.116992711 HST -10:00
# t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15.116992711 HST -10:00
- 23
def change(options)
if options[:zone] && options[:offset]
raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}"
end
new_time = time.change(options)
if options[:zone]
new_zone = ::Time.find_zone(options[:zone])
elsif options[:offset]
new_zone = ::Time.find_zone(new_time.utc_offset)
end
new_zone ||= time_zone
periods = new_zone.periods_for_local(new_time)
self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil)
end
# Uses Date to provide precise Time calculations for years, months, and days
# according to the proleptic Gregorian calendar. The result is returned as a
# new TimeWithZone object.
#
# The +options+ parameter takes a hash with any of these keys:
# <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>,
# <tt>:hours</tt>, <tt>:minutes</tt>, <tt>:seconds</tt>.
#
# If advancing by a value of variable length (i.e., years, weeks, months,
# days), move forward from #time, otherwise move forward from #utc, for
# accuracy when moving across DST boundaries.
#
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28.558049687 EDT -04:00
# now.advance(seconds: 1) # => Sun, 02 Nov 2014 01:26:29.558049687 EDT -04:00
# now.advance(minutes: 1) # => Sun, 02 Nov 2014 01:27:28.558049687 EDT -04:00
# now.advance(hours: 1) # => Sun, 02 Nov 2014 01:26:28.558049687 EST -05:00
# now.advance(days: 1) # => Mon, 03 Nov 2014 01:26:28.558049687 EST -05:00
# now.advance(weeks: 1) # => Sun, 09 Nov 2014 01:26:28.558049687 EST -05:00
# now.advance(months: 1) # => Tue, 02 Dec 2014 01:26:28.558049687 EST -05:00
# now.advance(years: 1) # => Mon, 02 Nov 2015 01:26:28.558049687 EST -05:00
- 23
def advance(options)
# If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time,
# otherwise advance from #utc, for accuracy when moving across DST boundaries
if options.values_at(:years, :weeks, :months, :days).any?
method_missing(:advance, options)
else
utc.advance(options).in_time_zone(time_zone)
end
end
- 23
%w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name|
- 299
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method_name} # def month
time.#{method_name} # time.month
end # end
EOV
end
# Returns Array of parts of Time in sequence of
# [seconds, minutes, hours, day, month, year, weekday, yearday, dst?, zone].
#
# now = Time.zone.now # => Tue, 18 Aug 2015 02:29:27.485278555 UTC +00:00
# now.to_a # => [27, 29, 2, 18, 8, 2015, 2, 230, false, "UTC"]
- 23
def to_a
[time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
end
# Returns the object's date and time as a floating point number of seconds
# since the Epoch (January 1, 1970 00:00 UTC).
#
# Time.zone.now.to_f # => 1417709320.285418
- 23
def to_f
utc.to_f
end
# Returns the object's date and time as an integer number of seconds
# since the Epoch (January 1, 1970 00:00 UTC).
#
# Time.zone.now.to_i # => 1417709320
- 23
def to_i
utc.to_i
end
- 23
alias_method :tv_sec, :to_i
# Returns the object's date and time as a rational number of seconds
# since the Epoch (January 1, 1970 00:00 UTC).
#
# Time.zone.now.to_r # => (708854548642709/500000)
- 23
def to_r
utc.to_r
end
# Returns an instance of DateTime with the timezone's UTC offset
#
# Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000
# Time.current.in_time_zone('Hawaii').to_datetime # => Mon, 17 Aug 2015 16:32:20 -1000
- 23
def to_datetime
@to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
end
# Returns an instance of +Time+, either with the same UTC offset
# as +self+ or in the local system timezone depending on the setting
# of +ActiveSupport.to_time_preserves_timezone+.
- 23
def to_time
if preserve_timezone
@to_time_with_instance_offset ||= getlocal(utc_offset)
else
@to_time_with_system_offset ||= getlocal
end
end
# So that +self+ <tt>acts_like?(:time)</tt>.
- 23
def acts_like_time?
true
end
# Say we're a Time to thwart type checking.
- 23
def is_a?(klass)
klass == ::Time || super
end
- 23
alias_method :kind_of?, :is_a?
# An instance of ActiveSupport::TimeWithZone is never blank
- 23
def blank?
false
end
- 23
def freeze
# preload instance variables before freezing
period; utc; time; to_datetime; to_time
super
end
- 23
def marshal_dump
[utc, time_zone.name, time]
end
- 23
def marshal_load(variables)
initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc)
end
# respond_to_missing? is not called in some cases, such as when type conversion is
# performed with Kernel#String
- 23
def respond_to?(sym, include_priv = false)
# ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow
return false if sym.to_sym == :to_str
super
end
# Ensure proxy class responds to all methods that underlying time instance
# responds to.
- 23
def respond_to_missing?(sym, include_priv)
return false if sym.to_sym == :acts_like_date?
time.respond_to?(sym, include_priv)
end
# Send the missing method to +time+ instance, and wrap result in a new
# TimeWithZone with the existing +time_zone+.
- 23
def method_missing(sym, *args, &block)
wrap_with_time_zone time.__send__(sym, *args, &block)
rescue NoMethodError => e
raise e, e.message.sub(time.inspect, inspect), e.backtrace
end
- 23
private
- 23
SECONDS_PER_DAY = 86400
- 23
def incorporate_utc_offset(time, offset)
if time.kind_of?(Date)
time + Rational(offset, SECONDS_PER_DAY)
else
time + offset
end
end
- 23
def get_period_and_ensure_valid_local_time(period)
# we don't want a Time.local instance enforcing its own DST rules as well,
# so transfer time values to a utc constructor if necessary
@time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
begin
period || @time_zone.period_for_local(@time)
rescue ::TZInfo::PeriodNotFound
# time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
@time += 1.hour
retry
end
end
- 23
def transfer_time_values_to_utc_constructor(time)
# avoid creating another Time object if possible
return time if time.instance_of?(::Time) && time.utc?
::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec + time.subsec)
end
- 23
def duration_of_variable_length?(obj)
ActiveSupport::Duration === obj && obj.parts.any? { |p| [:years, :months, :weeks, :days].include?(p[0]) }
end
- 23
def wrap_with_time_zone(time)
if time.acts_like?(:time)
periods = time_zone.periods_for_local(time)
self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil)
elsif time.is_a?(Range)
wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end)
else
time
end
end
end
end
# frozen_string_literal: true
- 23
require "tzinfo"
- 23
require "concurrent/map"
- 23
module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
# It allows us to do the following:
#
# * Limit the set of zones provided by TZInfo to a meaningful subset of 134
# zones.
# * Retrieve and display zones with a friendlier name
# (e.g., "Eastern Time (US & Canada)" instead of "America/New_York").
# * Lazily load TZInfo::Timezone instances only when they're needed.
# * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+,
# +parse+, +at+ and +now+ methods.
#
# If you set <tt>config.time_zone</tt> in the Rails Application, you can
# access this TimeZone object via <tt>Time.zone</tt>:
#
# # application.rb:
# class Application < Rails::Application
# config.time_zone = 'Eastern Time (US & Canada)'
# end
#
# Time.zone # => #<ActiveSupport::TimeZone:0x514834...>
# Time.zone.name # => "Eastern Time (US & Canada)"
# Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00
- 23
class TimeZone
# Keys are Rails TimeZone names, values are TZInfo identifiers.
- 23
MAPPING = {
"International Date Line West" => "Etc/GMT+12",
"Midway Island" => "Pacific/Midway",
"American Samoa" => "Pacific/Pago_Pago",
"Hawaii" => "Pacific/Honolulu",
"Alaska" => "America/Juneau",
"Pacific Time (US & Canada)" => "America/Los_Angeles",
"Tijuana" => "America/Tijuana",
"Mountain Time (US & Canada)" => "America/Denver",
"Arizona" => "America/Phoenix",
"Chihuahua" => "America/Chihuahua",
"Mazatlan" => "America/Mazatlan",
"Central Time (US & Canada)" => "America/Chicago",
"Saskatchewan" => "America/Regina",
"Guadalajara" => "America/Mexico_City",
"Mexico City" => "America/Mexico_City",
"Monterrey" => "America/Monterrey",
"Central America" => "America/Guatemala",
"Eastern Time (US & Canada)" => "America/New_York",
"Indiana (East)" => "America/Indiana/Indianapolis",
"Bogota" => "America/Bogota",
"Lima" => "America/Lima",
"Quito" => "America/Lima",
"Atlantic Time (Canada)" => "America/Halifax",
"Caracas" => "America/Caracas",
"La Paz" => "America/La_Paz",
"Santiago" => "America/Santiago",
"Newfoundland" => "America/St_Johns",
"Brasilia" => "America/Sao_Paulo",
"Buenos Aires" => "America/Argentina/Buenos_Aires",
"Montevideo" => "America/Montevideo",
"Georgetown" => "America/Guyana",
"Puerto Rico" => "America/Puerto_Rico",
"Greenland" => "America/Godthab",
"Mid-Atlantic" => "Atlantic/South_Georgia",
"Azores" => "Atlantic/Azores",
"Cape Verde Is." => "Atlantic/Cape_Verde",
"Dublin" => "Europe/Dublin",
"Edinburgh" => "Europe/London",
"Lisbon" => "Europe/Lisbon",
"London" => "Europe/London",
"Casablanca" => "Africa/Casablanca",
"Monrovia" => "Africa/Monrovia",
"UTC" => "Etc/UTC",
"Belgrade" => "Europe/Belgrade",
"Bratislava" => "Europe/Bratislava",
"Budapest" => "Europe/Budapest",
"Ljubljana" => "Europe/Ljubljana",
"Prague" => "Europe/Prague",
"Sarajevo" => "Europe/Sarajevo",
"Skopje" => "Europe/Skopje",
"Warsaw" => "Europe/Warsaw",
"Zagreb" => "Europe/Zagreb",
"Brussels" => "Europe/Brussels",
"Copenhagen" => "Europe/Copenhagen",
"Madrid" => "Europe/Madrid",
"Paris" => "Europe/Paris",
"Amsterdam" => "Europe/Amsterdam",
"Berlin" => "Europe/Berlin",
"Bern" => "Europe/Zurich",
"Zurich" => "Europe/Zurich",
"Rome" => "Europe/Rome",
"Stockholm" => "Europe/Stockholm",
"Vienna" => "Europe/Vienna",
"West Central Africa" => "Africa/Algiers",
"Bucharest" => "Europe/Bucharest",
"Cairo" => "Africa/Cairo",
"Helsinki" => "Europe/Helsinki",
"Kyiv" => "Europe/Kiev",
"Riga" => "Europe/Riga",
"Sofia" => "Europe/Sofia",
"Tallinn" => "Europe/Tallinn",
"Vilnius" => "Europe/Vilnius",
"Athens" => "Europe/Athens",
"Istanbul" => "Europe/Istanbul",
"Minsk" => "Europe/Minsk",
"Jerusalem" => "Asia/Jerusalem",
"Harare" => "Africa/Harare",
"Pretoria" => "Africa/Johannesburg",
"Kaliningrad" => "Europe/Kaliningrad",
"Moscow" => "Europe/Moscow",
"St. Petersburg" => "Europe/Moscow",
"Volgograd" => "Europe/Volgograd",
"Samara" => "Europe/Samara",
"Kuwait" => "Asia/Kuwait",
"Riyadh" => "Asia/Riyadh",
"Nairobi" => "Africa/Nairobi",
"Baghdad" => "Asia/Baghdad",
"Tehran" => "Asia/Tehran",
"Abu Dhabi" => "Asia/Muscat",
"Muscat" => "Asia/Muscat",
"Baku" => "Asia/Baku",
"Tbilisi" => "Asia/Tbilisi",
"Yerevan" => "Asia/Yerevan",
"Kabul" => "Asia/Kabul",
"Ekaterinburg" => "Asia/Yekaterinburg",
"Islamabad" => "Asia/Karachi",
"Karachi" => "Asia/Karachi",
"Tashkent" => "Asia/Tashkent",
"Chennai" => "Asia/Kolkata",
"Kolkata" => "Asia/Kolkata",
"Mumbai" => "Asia/Kolkata",
"New Delhi" => "Asia/Kolkata",
"Kathmandu" => "Asia/Kathmandu",
"Astana" => "Asia/Dhaka",
"Dhaka" => "Asia/Dhaka",
"Sri Jayawardenepura" => "Asia/Colombo",
"Almaty" => "Asia/Almaty",
"Novosibirsk" => "Asia/Novosibirsk",
"Rangoon" => "Asia/Rangoon",
"Bangkok" => "Asia/Bangkok",
"Hanoi" => "Asia/Bangkok",
"Jakarta" => "Asia/Jakarta",
"Krasnoyarsk" => "Asia/Krasnoyarsk",
"Beijing" => "Asia/Shanghai",
"Chongqing" => "Asia/Chongqing",
"Hong Kong" => "Asia/Hong_Kong",
"Urumqi" => "Asia/Urumqi",
"Kuala Lumpur" => "Asia/Kuala_Lumpur",
"Singapore" => "Asia/Singapore",
"Taipei" => "Asia/Taipei",
"Perth" => "Australia/Perth",
"Irkutsk" => "Asia/Irkutsk",
"Ulaanbaatar" => "Asia/Ulaanbaatar",
"Seoul" => "Asia/Seoul",
"Osaka" => "Asia/Tokyo",
"Sapporo" => "Asia/Tokyo",
"Tokyo" => "Asia/Tokyo",
"Yakutsk" => "Asia/Yakutsk",
"Darwin" => "Australia/Darwin",
"Adelaide" => "Australia/Adelaide",
"Canberra" => "Australia/Melbourne",
"Melbourne" => "Australia/Melbourne",
"Sydney" => "Australia/Sydney",
"Brisbane" => "Australia/Brisbane",
"Hobart" => "Australia/Hobart",
"Vladivostok" => "Asia/Vladivostok",
"Guam" => "Pacific/Guam",
"Port Moresby" => "Pacific/Port_Moresby",
"Magadan" => "Asia/Magadan",
"Srednekolymsk" => "Asia/Srednekolymsk",
"Solomon Is." => "Pacific/Guadalcanal",
"New Caledonia" => "Pacific/Noumea",
"Fiji" => "Pacific/Fiji",
"Kamchatka" => "Asia/Kamchatka",
"Marshall Is." => "Pacific/Majuro",
"Auckland" => "Pacific/Auckland",
"Wellington" => "Pacific/Auckland",
"Nuku'alofa" => "Pacific/Tongatapu",
"Tokelau Is." => "Pacific/Fakaofo",
"Chatham Is." => "Pacific/Chatham",
"Samoa" => "Pacific/Apia"
}
- 23
UTC_OFFSET_WITH_COLON = "%s%02d:%02d" # :nodoc:
- 23
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") # :nodoc:
- 23
private_constant :UTC_OFFSET_WITH_COLON, :UTC_OFFSET_WITHOUT_COLON
- 23
@lazy_zones_map = Concurrent::Map.new
- 23
@country_zones = Concurrent::Map.new
- 23
class << self
# Assumes self represents an offset from UTC in seconds (as returned from
# Time#utc_offset) and turns this into an +HH:MM formatted string.
#
# ActiveSupport::TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
- 23
def seconds_to_utc_offset(seconds, colon = true)
format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
sign = (seconds < 0 ? "-" : "+")
hours = seconds.abs / 3600
minutes = (seconds.abs % 3600) / 60
format % [sign, hours, minutes]
end
- 23
def find_tzinfo(name)
- 151
TZInfo::Timezone.get(MAPPING[name] || name)
end
- 23
alias_method :create, :new
# Returns a TimeZone instance with the given name, or +nil+ if no
# such TimeZone instance exists. (This exists to support the use of
# this class with the +composed_of+ macro.)
- 23
def new(name)
self[name]
end
# Returns an array of all TimeZone objects. There are multiple
# TimeZone objects per time zone, in many cases, to make it easier
# for users to find their own time zone.
- 23
def all
- 1
@zones ||= zones_map.values.sort
end
# Locate a specific time zone object. If the argument is a string, it
# is interpreted to mean the name of the timezone to locate. If it is a
# numeric value it is either the hour offset, or the second offset, of the
# timezone to find. (The first one with that offset will be returned.)
# Returns +nil+ if no such time zone is known to the system.
- 23
def [](arg)
- 151
case arg
when String
- 151
begin
- 151
@lazy_zones_map[arg] ||= create(arg)
rescue TZInfo::InvalidTimezoneIdentifier
nil
end
when Numeric, ActiveSupport::Duration
arg *= 3600 if arg.abs <= 13
all.find { |z| z.utc_offset == arg.to_i }
else
raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
end
end
# A convenience method for returning a collection of TimeZone objects
# for time zones in the USA.
- 23
def us_zones
country_zones(:us)
end
# A convenience method for returning a collection of TimeZone objects
# for time zones in the country specified by its ISO 3166-1 Alpha2 code.
- 23
def country_zones(country_code)
code = country_code.to_s.upcase
@country_zones[code] ||= load_country_zones(code)
end
- 23
def clear #:nodoc:
@lazy_zones_map = Concurrent::Map.new
@country_zones = Concurrent::Map.new
@zones = nil
@zones_map = nil
end
- 23
private
- 23
def load_country_zones(code)
country = TZInfo::Country.get(code)
country.zone_identifiers.flat_map do |tz_id|
if MAPPING.value?(tz_id)
MAPPING.inject([]) do |memo, (key, value)|
memo << self[key] if value == tz_id
memo
end
else
create(tz_id, nil, TZInfo::Timezone.get(tz_id))
end
end.sort!
end
- 23
def zones_map
- 1
@zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones|
- 151
timezone = self[name]
- 151
zones[name] = timezone if timezone
end
end
end
- 23
include Comparable
- 23
attr_reader :name
- 23
attr_reader :tzinfo
# Create a new TimeZone object with the given name and offset. The
# offset is the number of seconds that this time zone is offset from UTC
# (GMT). Seconds were chosen as the offset unit because that is the unit
# that Ruby uses to represent time zone offsets (see Time#utc_offset).
- 23
def initialize(name, utc_offset = nil, tzinfo = nil)
- 151
@name = name
- 151
@utc_offset = utc_offset
- 151
@tzinfo = tzinfo || TimeZone.find_tzinfo(name)
end
# Returns the offset of this time zone from UTC in seconds.
- 23
def utc_offset
- 1996
@utc_offset || tzinfo&.current_period&.base_utc_offset
end
# 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"
- 23
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
# Compare this time zone to the parameter. The two are compared first on
# their offsets, and then by name.
- 23
def <=>(zone)
- 998
return unless zone.respond_to? :utc_offset
- 998
result = (utc_offset <=> zone.utc_offset)
- 998
result = (name <=> zone.name) if result == 0
- 998
result
end
# Compare #name and TZInfo identifier to a supplied regexp, returning +true+
# if a match is found.
- 23
def =~(re)
re === name || re === MAPPING[name]
end
# Compare #name and TZInfo identifier to a supplied regexp, returning +true+
# if a match is found.
- 23
def match?(re)
(re == name) || (re == MAPPING[name]) ||
((Regexp === re) && (re.match?(name) || re.match?(MAPPING[name])))
end
# Returns a textual representation of this time zone.
- 23
def to_s
"(GMT#{formatted_offset}) #{name}"
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from given values.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
- 23
def local(*args)
time = Time.utc(*args)
ActiveSupport::TimeWithZone.new(nil, self, time)
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from number of seconds since the Unix epoch.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.utc(2000).to_f # => 946684800.0
# Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00
#
# A second argument can be supplied to specify sub-second precision.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.at(946684800, 123456.789).nsec # => 123456789
- 23
def at(*args)
Time.at(*args).utc.in_time_zone(self)
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from an ISO 8601 string.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.iso8601('1999-12-31T14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
#
# If the time components are missing then they will be set to zero.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.iso8601('1999-12-31') # => Fri, 31 Dec 1999 00:00:00 HST -10:00
#
# If the string is invalid then an +ArgumentError+ will be raised unlike +parse+
# which usually returns +nil+ when given an invalid date string.
- 23
def iso8601(str)
parts = Date._iso8601(str)
raise ArgumentError, "invalid date" if parts.empty?
time = Time.new(
parts.fetch(:year),
parts.fetch(:mon),
parts.fetch(:mday),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
parts.fetch(:offset, 0)
)
if parts[:offset]
TimeWithZone.new(time.utc, self)
else
TimeWithZone.new(nil, self, time)
end
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from parsed string.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.parse('1999-12-31 14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
#
# If upper components are missing from the string, they are supplied from
# TimeZone#now:
#
# Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
#
# However, if the date component is not provided, but any other upper
# components are supplied, then the day of the month defaults to 1:
#
# Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
#
# If the string is invalid then an +ArgumentError+ could be raised.
- 23
def parse(str, now = now())
parts_to_time(Date._parse(str, false), now)
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from an RFC 3339 string.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.rfc3339('2000-01-01T00:00:00Z') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
#
# If the time or zone components are missing then an +ArgumentError+ will
# be raised. This is much stricter than either +parse+ or +iso8601+ which
# allow for missing components.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.rfc3339('1999-12-31') # => ArgumentError: invalid date
- 23
def rfc3339(str)
parts = Date._rfc3339(str)
raise ArgumentError, "invalid date" if parts.empty?
time = Time.new(
parts.fetch(:year),
parts.fetch(:mon),
parts.fetch(:mday),
parts.fetch(:hour),
parts.fetch(:min),
parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
parts.fetch(:offset)
)
TimeWithZone.new(time.utc, self)
end
# Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone.
#
# Assumes that +str+ is a time in the time zone +self+,
# unless +format+ includes an explicit time zone.
# (This is the same behavior as +parse+.)
# In either case, the returned TimeWithZone has the timezone of +self+.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.strptime('1999-12-31 14:00:00', '%Y-%m-%d %H:%M:%S') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
#
# If upper components are missing from the string, they are supplied from
# TimeZone#now:
#
# Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Time.zone.strptime('22:30:00', '%H:%M:%S') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
#
# However, if the date component is not provided, but any other upper
# components are supplied, then the day of the month defaults to 1:
#
# Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
- 23
def strptime(str, format, now = now())
parts_to_time(DateTime._strptime(str, format), now)
end
# Returns an ActiveSupport::TimeWithZone instance representing the current
# time in the time zone represented by +self+.
#
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.now # => Wed, 23 Jan 2008 20:24:27 HST -10:00
- 23
def now
time_now.utc.in_time_zone(self)
end
# Returns the current date in this time zone.
- 23
def today
tzinfo.now.to_date
end
# Returns the next date in this time zone.
- 23
def tomorrow
today + 1
end
# Returns the previous date in this time zone.
- 23
def yesterday
today - 1
end
# Adjust the given time to the simultaneous time in the time zone
# represented by +self+. Returns a local time with the appropriate offset
# -- if you want an ActiveSupport::TimeWithZone instance, use
# Time#in_time_zone() instead.
#
# As of tzinfo 2, utc_to_local returns a Time with a non-zero utc_offset.
# See the `utc_to_local_returns_utc_offset_times` config for more info.
- 23
def utc_to_local(time)
tzinfo.utc_to_local(time).yield_self do |t|
ActiveSupport.utc_to_local_returns_utc_offset_times ?
t : Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, t.sec_fraction)
end
end
# Adjust the given time to the simultaneous time in UTC. Returns a
# Time.utc() instance.
- 23
def local_to_utc(time, dst = true)
tzinfo.local_to_utc(time, dst)
end
# Available so that TimeZone instances respond like TZInfo::Timezone
# instances.
- 23
def period_for_utc(time)
tzinfo.period_for_utc(time)
end
# Available so that TimeZone instances respond like TZInfo::Timezone
# instances.
- 23
def period_for_local(time, dst = true)
tzinfo.period_for_local(time, dst) { |periods| periods.last }
end
- 23
def periods_for_local(time) #:nodoc:
tzinfo.periods_for_local(time)
end
- 23
def init_with(coder) #:nodoc:
initialize(coder["name"])
end
- 23
def encode_with(coder) #:nodoc:
coder.tag = "!ruby/object:#{self.class}"
coder.map = { "name" => tzinfo.name }
end
- 23
private
- 23
def parts_to_time(parts, now)
raise ArgumentError, "invalid date" if parts.nil?
return if parts.empty?
if parts[:seconds]
time = Time.at(parts[:seconds])
else
time = Time.new(
parts.fetch(:year, now.year),
parts.fetch(:mon, now.month),
parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
parts.fetch(:offset, 0)
)
end
if parts[:offset] || parts[:seconds]
TimeWithZone.new(time.utc, self)
else
TimeWithZone.new(nil, self, time)
end
end
- 23
def time_now
Time.now
end
end
end
# frozen_string_literal: true
- 24
require_relative "gem_version"
- 24
module ActiveSupport
# Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
- 24
def self.version
gem_version
end
end
# frozen_string_literal: true
- 23
require "time"
- 23
require "base64"
- 23
require "bigdecimal"
- 23
require "bigdecimal/util"
- 23
require "active_support/core_ext/module/delegation"
- 23
require "active_support/core_ext/string/inflections"
- 23
require "active_support/core_ext/date_time/calculations"
- 23
module ActiveSupport
# = XmlMini
#
# To use the much faster libxml parser:
# gem 'libxml-ruby', '=0.9.7'
# XmlMini.backend = 'LibXML'
- 23
module XmlMini
- 23
extend self
# This module decorates files deserialized using Hash.from_xml with
# the <tt>original_filename</tt> and <tt>content_type</tt> methods.
- 23
module FileLike #:nodoc:
- 23
attr_writer :original_filename, :content_type
- 23
def original_filename
@original_filename || "untitled"
end
- 23
def content_type
@content_type || "application/octet-stream"
end
end
DEFAULT_ENCODINGS = {
"binary" => "base64"
- 23
} unless defined?(DEFAULT_ENCODINGS)
- 23
unless defined?(TYPE_NAMES)
- 23
TYPE_NAMES = {
"Symbol" => "symbol",
"Integer" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
"TrueClass" => "boolean",
"FalseClass" => "boolean",
"Date" => "date",
"DateTime" => "dateTime",
"Time" => "dateTime",
"Array" => "array",
"Hash" => "hash"
}
end
FORMATTING = {
"symbol" => Proc.new { |symbol| symbol.to_s },
"date" => Proc.new { |date| date.to_s(:db) },
"dateTime" => Proc.new { |time| time.xmlschema },
"binary" => Proc.new { |binary| ::Base64.encode64(binary) },
"yaml" => Proc.new { |yaml| yaml.to_yaml }
- 23
} unless defined?(FORMATTING)
# TODO use regexp instead of Date.parse
- 23
unless defined?(PARSING)
- 23
PARSING = {
"symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
"decimal" => Proc.new do |number|
if String === number
number.to_d
else
BigDecimal(number)
end
end,
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
"binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
}
- 23
PARSING.update(
"double" => PARSING["float"],
"dateTime" => PARSING["datetime"]
)
end
- 23
attr_accessor :depth
- 23
self.depth = 100
- 23
delegate :parse, to: :backend
- 23
def backend
current_thread_backend || @backend
end
- 23
def backend=(name)
- 23
backend = name && cast_backend_name_to_module(name)
- 23
self.current_thread_backend = backend if current_thread_backend
- 23
@backend = backend
end
- 23
def with_backend(name)
old_backend = current_thread_backend
self.current_thread_backend = name && cast_backend_name_to_module(name)
yield
ensure
self.current_thread_backend = old_backend
end
- 23
def to_tag(key, value, options)
type_name = options.delete(:type)
merged_options = options.merge(root: key, skip_instruct: true)
if value.is_a?(::Method) || value.is_a?(::Proc)
if value.arity == 1
value.call(merged_options)
else
value.call(merged_options, key.to_s.singularize)
end
elsif value.respond_to?(:to_xml)
value.to_xml(merged_options)
else
type_name ||= TYPE_NAMES[value.class.name]
type_name ||= value.class.name if value && !value.respond_to?(:to_str)
type_name = type_name.to_s if type_name
type_name = "dateTime" if type_name == "datetime"
key = rename_key(key.to_s, options)
attributes = options[:skip_types] || type_name.nil? ? {} : { type: type_name }
attributes[:nil] = true if value.nil?
encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name]
attributes[:encoding] = encoding if encoding
formatted_value = FORMATTING[type_name] && !value.nil? ?
FORMATTING[type_name].call(value) : value
options[:builder].tag!(key, formatted_value, attributes)
end
end
- 23
def rename_key(key, options = {})
camelize = options[:camelize]
dasherize = !options.has_key?(:dasherize) || options[:dasherize]
if camelize
key = true == camelize ? key.camelize : key.camelize(camelize)
end
key = _dasherize(key) if dasherize
key
end
- 23
private
- 23
def _dasherize(key)
# $2 must be a non-greedy regex for this to work
left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1, 3]
"#{left}#{middle.tr('_ ', '--')}#{right}"
end
# TODO: Add support for other encodings
- 23
def _parse_binary(bin, entity)
case entity["encoding"]
when "base64"
::Base64.decode64(bin)
else
bin
end
end
- 23
def _parse_file(file, entity)
f = StringIO.new(::Base64.decode64(file))
f.extend(FileLike)
f.original_filename = entity["name"]
f.content_type = entity["content_type"]
f
end
- 23
def current_thread_backend
- 23
Thread.current[:xml_mini_backend]
end
- 23
def current_thread_backend=(name)
Thread.current[:xml_mini_backend] = name && cast_backend_name_to_module(name)
end
- 23
def cast_backend_name_to_module(name)
- 23
if name.is_a?(Module)
name
else
- 23
require "active_support/xml_mini/#{name.downcase}"
- 23
ActiveSupport.const_get("XmlMini_#{name}")
end
end
end
- 23
XmlMini.backend = "REXML"
end
# frozen_string_literal: true
raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFORM.include?("java")
require "jruby"
include Java
require "active_support/core_ext/object/blank"
java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder
java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory
java_import java.io.StringReader unless defined? StringReader
java_import org.xml.sax.InputSource unless defined? InputSource
java_import org.xml.sax.Attributes unless defined? Attributes
java_import org.w3c.dom.Node unless defined? Node
module ActiveSupport
module XmlMini_JDOM #:nodoc:
extend self
CONTENT_KEY = "__content__"
NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE
DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE
PROCESSING_INSTRUCTION_NODE TEXT_NODE}
node_type_map = {}
NODE_TYPE_NAMES.each { |type| node_type_map[Node.send(type)] = type }
# Parse an XML Document string or IO into a simple hash using Java's jdom.
# data::
# XML Document string or IO to parse
def parse(data)
if data.respond_to?(:read)
data = data.read
end
if data.blank?
{}
else
@dbf = DocumentBuilderFactory.new_instance
# secure processing of java xml
# https://archive.is/9xcQQ
@dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
@dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
@dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
@dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true)
xml_string_reader = StringReader.new(data)
xml_input_source = InputSource.new(xml_string_reader)
doc = @dbf.new_document_builder.parse(xml_input_source)
merge_element!({ CONTENT_KEY => "" }, doc.document_element, XmlMini.depth)
end
end
private
# Convert an XML element and merge into the hash
#
# hash::
# Hash to merge the converted element into.
# element::
# XML element to merge into hash
def merge_element!(hash, element, depth)
raise "Document too deep!" if depth == 0
delete_empty(hash)
merge!(hash, element.tag_name, collapse(element, depth))
end
def delete_empty(hash)
hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ""
end
# Actually converts an XML document element into a data structure.
#
# element::
# The document element to be collapsed.
def collapse(element, depth)
hash = get_attributes(element)
child_nodes = element.child_nodes
if child_nodes.length > 0
(0...child_nodes.length).each do |i|
child = child_nodes.item(i)
merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
end
merge_texts!(hash, element) unless empty_content?(element)
hash
else
merge_texts!(hash, element)
end
end
# Merge all the texts of an element into the hash
#
# hash::
# Hash to add the converted element to.
# element::
# XML element whose texts are to me merged into the hash
def merge_texts!(hash, element)
delete_empty(hash)
text_children = texts(element)
if text_children.join.empty?
hash
else
# must use value to prevent double-escaping
merge!(hash, CONTENT_KEY, text_children.join)
end
end
# Adds a new key/value pair to an existing Hash. If the key to be added
# already exists and the existing value associated with key is not
# an Array, it will be wrapped in an Array. Then the new value is
# appended to that Array.
#
# hash::
# Hash to add key/value pair to.
# key::
# Key to be added.
# value::
# Value to be associated with key.
def merge!(hash, key, value)
if hash.has_key?(key)
if hash[key].instance_of?(Array)
hash[key] << value
else
hash[key] = [hash[key], value]
end
elsif value.instance_of?(Array)
hash[key] = [value]
else
hash[key] = value
end
hash
end
# Converts the attributes array of an XML element into a hash.
# Returns an empty Hash if node has no attributes.
#
# element::
# XML element to extract attributes from.
def get_attributes(element)
attribute_hash = {}
attributes = element.attributes
(0...attributes.length).each do |i|
attribute_hash[CONTENT_KEY] ||= ""
attribute_hash[attributes.item(i).name] = attributes.item(i).value
end
attribute_hash
end
# Determines if a document element has text content
#
# element::
# XML element to be checked.
def texts(element)
texts = []
child_nodes = element.child_nodes
(0...child_nodes.length).each do |i|
item = child_nodes.item(i)
if item.node_type == Node.TEXT_NODE
texts << item.get_data
end
end
texts
end
# Determines if a document element has text content
#
# element::
# XML element to be checked.
def empty_content?(element)
text = +""
child_nodes = element.child_nodes
(0...child_nodes.length).each do |i|
item = child_nodes.item(i)
if item.node_type == Node.TEXT_NODE
text << item.get_data.strip
end
end
text.strip.length == 0
end
end
end
# frozen_string_literal: true
require "libxml"
require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
module XmlMini_LibXML #:nodoc:
extend self
# Parse an XML Document string or IO into a simple hash using libxml.
# data::
# XML Document string or IO to parse
def parse(data)
if !data.respond_to?(:read)
data = StringIO.new(data || "")
end
if data.eof?
{}
else
LibXML::XML::Parser.io(data).parse.to_hash
end
end
end
end
module LibXML #:nodoc:
module Conversions #:nodoc:
module Document #:nodoc:
def to_hash
root.to_hash
end
end
module Node #:nodoc:
CONTENT_ROOT = "__content__"
# Convert XML document to hash.
#
# hash::
# Hash to merge the converted element into.
def to_hash(hash = {})
node_hash = {}
# Insert node hash into parent hash correctly.
case hash[name]
when Array then hash[name] << node_hash
when Hash then hash[name] = [hash[name], node_hash]
when nil then hash[name] = node_hash
end
# Handle child elements
each_child do |c|
if c.element?
c.to_hash(node_hash)
elsif c.text? || c.cdata?
node_hash[CONTENT_ROOT] ||= +""
node_hash[CONTENT_ROOT] << c.content
end
end
# Remove content node if it is blank
if node_hash.length > 1 && node_hash[CONTENT_ROOT].blank?
node_hash.delete(CONTENT_ROOT)
end
# Handle attributes
each_attr { |a| node_hash[a.name] = a.value }
hash
end
end
end
end
# :enddoc:
LibXML::XML::Document.include(LibXML::Conversions::Document)
LibXML::XML::Node.include(LibXML::Conversions::Node)
# frozen_string_literal: true
require "libxml"
require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
module XmlMini_LibXMLSAX #:nodoc:
extend self
# Class that will build the hash while the XML document
# is being parsed using SAX events.
class HashBuilder
include LibXML::XML::SaxParser::Callbacks
CONTENT_KEY = "__content__"
HASH_SIZE_KEY = "__hash_size__"
attr_reader :hash
def current_hash
@hash_stack.last
end
def on_start_document
@hash = { CONTENT_KEY => +"" }
@hash_stack = [@hash]
end
def on_end_document
@hash = @hash_stack.pop
@hash.delete(CONTENT_KEY)
end
def on_start_element(name, attrs = {})
new_hash = { CONTENT_KEY => +"" }.merge!(attrs)
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
when Array then current_hash[name] << new_hash
when Hash then current_hash[name] = [current_hash[name], new_hash]
when nil then current_hash[name] = new_hash
end
@hash_stack.push(new_hash)
end
def on_end_element(name)
if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
current_hash.delete(CONTENT_KEY)
end
@hash_stack.pop
end
def on_characters(string)
current_hash[CONTENT_KEY] << string
end
alias_method :on_cdata_block, :on_characters
end
attr_accessor :document_class
self.document_class = HashBuilder
def parse(data)
if !data.respond_to?(:read)
data = StringIO.new(data || "")
end
if data.eof?
{}
else
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
parser = LibXML::XML::SaxParser.io(data)
document = document_class.new
parser.callbacks = document
parser.parse
document.hash
end
end
end
end
# frozen_string_literal: true
begin
require "nokogiri"
rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
module XmlMini_Nokogiri #:nodoc:
extend self
# Parse an XML Document string or IO into a simple hash using libxml / nokogiri.
# data::
# XML Document string or IO to parse
def parse(data)
if !data.respond_to?(:read)
data = StringIO.new(data || "")
end
if data.eof?
{}
else
doc = Nokogiri::XML(data)
raise doc.errors.first if doc.errors.length > 0
doc.to_hash
end
end
module Conversions #:nodoc:
module Document #:nodoc:
def to_hash
root.to_hash
end
end
module Node #:nodoc:
CONTENT_ROOT = "__content__"
# Convert XML document to hash.
#
# hash::
# Hash to merge the converted element into.
def to_hash(hash = {})
node_hash = {}
# Insert node hash into parent hash correctly.
case hash[name]
when Array then hash[name] << node_hash
when Hash then hash[name] = [hash[name], node_hash]
when nil then hash[name] = node_hash
end
# Handle child elements
children.each do |c|
if c.element?
c.to_hash(node_hash)
elsif c.text? || c.cdata?
node_hash[CONTENT_ROOT] ||= +""
node_hash[CONTENT_ROOT] << c.content
end
end
# Remove content node if it is blank and there are child tags
if node_hash.length > 1 && node_hash[CONTENT_ROOT].blank?
node_hash.delete(CONTENT_ROOT)
end
# Handle attributes
attribute_nodes.each { |a| node_hash[a.node_name] = a.value }
hash
end
end
end
Nokogiri::XML::Document.include(Conversions::Document)
Nokogiri::XML::Node.include(Conversions::Node)
end
end
# frozen_string_literal: true
begin
require "nokogiri"
rescue LoadError => e
$stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
require "active_support/core_ext/object/blank"
require "stringio"
module ActiveSupport
module XmlMini_NokogiriSAX #:nodoc:
extend self
# Class that will build the hash while the XML document
# is being parsed using SAX events.
class HashBuilder < Nokogiri::XML::SAX::Document
CONTENT_KEY = "__content__"
HASH_SIZE_KEY = "__hash_size__"
attr_reader :hash
def current_hash
@hash_stack.last
end
def start_document
@hash = {}
@hash_stack = [@hash]
end
def end_document
raise "Parse stack not empty!" if @hash_stack.size > 1
end
def error(error_message)
raise error_message
end
def start_element(name, attrs = [])
new_hash = { CONTENT_KEY => +"" }.merge!(Hash[attrs])
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
when Array then current_hash[name] << new_hash
when Hash then current_hash[name] = [current_hash[name], new_hash]
when nil then current_hash[name] = new_hash
end
@hash_stack.push(new_hash)
end
def end_element(name)
if current_hash.length > current_hash.delete(HASH_SIZE_KEY) && current_hash[CONTENT_KEY].blank? || current_hash[CONTENT_KEY] == ""
current_hash.delete(CONTENT_KEY)
end
@hash_stack.pop
end
def characters(string)
current_hash[CONTENT_KEY] << string
end
alias_method :cdata_block, :characters
end
attr_accessor :document_class
self.document_class = HashBuilder
def parse(data)
if !data.respond_to?(:read)
data = StringIO.new(data || "")
end
if data.eof?
{}
else
document = document_class.new
parser = Nokogiri::XML::SAX::Parser.new(document)
parser.parse(data)
document.hash
end
end
end
end
# frozen_string_literal: true
- 23
require "active_support/core_ext/kernel/reporting"
- 23
require "active_support/core_ext/object/blank"
- 23
require "stringio"
- 23
module ActiveSupport
- 23
module XmlMini_REXML #:nodoc:
- 23
extend self
- 23
CONTENT_KEY = "__content__"
# Parse an XML Document string or IO into a simple hash.
#
# Same as XmlSimple::xml_in but doesn't shoot itself in the foot,
# and uses the defaults from Active Support.
#
# data::
# XML Document string or IO to parse
- 23
def parse(data)
if !data.respond_to?(:read)
data = StringIO.new(data || "")
end
if data.eof?
{}
else
silence_warnings { require "rexml/document" } unless defined?(REXML::Document)
doc = REXML::Document.new(data)
if doc.root
merge_element!({}, doc.root, XmlMini.depth)
else
raise REXML::ParseException,
"The document #{doc.to_s.inspect} does not have a valid root"
end
end
end
- 23
private
# Convert an XML element and merge into the hash
#
# hash::
# Hash to merge the converted element into.
# element::
# XML element to merge into hash
- 23
def merge_element!(hash, element, depth)
raise REXML::ParseException, "The document is too deep" if depth == 0
merge!(hash, element.name, collapse(element, depth))
end
# Actually converts an XML document element into a data structure.
#
# element::
# The document element to be collapsed.
- 23
def collapse(element, depth)
hash = get_attributes(element)
if element.has_elements?
element.each_element { |child| merge_element!(hash, child, depth - 1) }
merge_texts!(hash, element) unless empty_content?(element)
hash
else
merge_texts!(hash, element)
end
end
# Merge all the texts of an element into the hash
#
# hash::
# Hash to add the converted element to.
# element::
# XML element whose texts are to me merged into the hash
- 23
def merge_texts!(hash, element)
unless element.has_text?
hash
else
# must use value to prevent double-escaping
texts = +""
element.texts.each { |t| texts << t.value }
merge!(hash, CONTENT_KEY, texts)
end
end
# Adds a new key/value pair to an existing Hash. If the key to be added
# already exists and the existing value associated with key is not
# an Array, it will be wrapped in an Array. Then the new value is
# appended to that Array.
#
# hash::
# Hash to add key/value pair to.
# key::
# Key to be added.
# value::
# Value to be associated with key.
- 23
def merge!(hash, key, value)
if hash.has_key?(key)
if hash[key].instance_of?(Array)
hash[key] << value
else
hash[key] = [hash[key], value]
end
elsif value.instance_of?(Array)
hash[key] = [value]
else
hash[key] = value
end
hash
end
# Converts the attributes array of an XML element into a hash.
# Returns an empty Hash if node has no attributes.
#
# element::
# XML element to extract attributes from.
- 23
def get_attributes(element)
attributes = {}
element.attributes.each { |n, v| attributes[n] = v }
attributes
end
# Determines if a document element has text content
#
# element::
# XML element to be checked.
- 23
def empty_content?(element)
element.texts.join.blank?
end
end
end