Active-support: Outside & Inside
Research of rails standard library ActiveSupport. Realization review and some useful functions.
“It’s best to have your tools with you. If you don’t, you’re apt to find something you didn’t expect and get discouraged.” Stephen King, On Writing: A Memoir of the Craft
Introduction
All developers who build Rails applications use Active-Support. It extends Rails and every programmer has probably used it at one time:
... rails/rails.gemspec
21 s.files = ["README.md"]
22
23 s.add_dependency "activesupport", version
24 s.add_dependency "actionpack", version
25 s.add_dependency "actionview", version
But often, many do not realize when they use this gem. In this article, I want to recall some useful features that it brings to our Rails applications.
Gem
Active Support is a collection of extensions for standard Ruby classes. Mainly they are targeted at working with the Web. But it also has many other useful extensions that may come in handy in developing non-web applications.
Often when we generate Rails app most of the cool stuff is already available to us, because Active Support adds its extensions to standard Ruby classes. All changes, to which standard classes are exposed, can be viewed here.
https://github.com/rails/rails/tree/master/activesupport/lib/active_support/
Let’s see these changes in detail.
What we already had
Active Support contributes to converting objects to strings. Instead of the basic Ruby notation with ‘e’, we get a floating-point number.
irb
> require "bigdecimal"
=> true
> BigDecimal.new("0.2").to_s
=> "0.2e0"
rails c
> BigDecimal.new("0.2").to_s
=> "0.2"
Range class was also changed. Range.to_s
method assumes the argument in one of the formats. Now there is only one available format - :db.
> (Date.today..Date.tomorrow).to_s
=> "2018-04-27..2018-04-28"
> (Date.today..Date.tomorrow).to_s(:db)
=> "BETWEEN '2018-04-27' AND '2018-04-28'"
The module developers also paid attention to Range.include?
method by adding the ability to pass a different range as an argument. Thereby they check nesting of one range in another (by entry of interval end-points into recipient interval)
ruby > (Date.yesterday..Date.tomorrow).include?((Date.today..Date.tomorrow) ) => true
We are all aware of Array.slice
method. Active Support also adds this function in Hash
.
It works the same as for Array, but uses keys instead of indexes.
ruby > [1,2,3,4].slice((1..2)) => [2, 3] > {a: 1, b: 2, c: 3}.slice(:a, :c) => {:a=>1, :c=>3}
What we got used to
Mostly we use present?
and blank?
methods. And yes, these methods are not in standard Ruby collection. These methods are provided in our Rail application by Active Support.
#...rails/activesupport/lib/active_support/core_ext/object/blank.rb
19 def blank?
20 respond_to?(:empty?) ? !!empty? : !self
21 end
26 def present?
27 !blank?
28 end
As you can see from the source code, this is a test for empty? (Method from Object
).
Active Support also defines the behavior for the following basic class methods:
NilClass
, FalseClass
, TrueClass
, Array
, Hash
, String
, Numeric
, Time
.
Another method is symbolize_keys
.It uses a more extended version of working with keys in
Hash: transform_keys
25 def transform_keys!
26 return enum_for(:transform_keys!) { size } unless block_given?
27 keys.each do |key|
28 self[yield(key)] = delete(key)
29 end
30 self
31 end unless method_defined? :transform_keys!
As you can see from the code, we create a new key by running the block over the old key, simultaneously getting the value from the old key and deleting it. Simple and elegant!
A wonderful drapper gem, that is used to implement decorator design pattern, actively uses the
delegate
functionality.
#draper/draper.gemspec
20 s.add_dependency 'activesupport', '~> 5.0'
# ... rails/activesupport/lib/active_support/core_ext/module/delegation.rb
157 def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
158 unless to
We should mention that this extension does not use standard Module::Forwardable
.
Thanks to Active Support there is a “pseudo object functionality” in Rails (something like Hashie::Mash). There is no magic in Rails configuration - only Hash:
config.active_storage = ActiveSupport::OrderedOptions.new
...
config.action_mailer = ActiveSupport::OrderedOptions.new
...
config.active_job = ActiveSupport::OrderedOptions.new
...
config.i18n = ActiveSupport::OrderedOptions.new
...
config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
...
config.action_controller = ActiveSupport::OrderedOptions.new
...
config.active_support = ActiveSupport::OrderedOptions.new
...
config.active_record = ActiveSupport::OrderedOptions.new
...
#https://github.com/rails/rails/blob/master/railties/lib/rails/application/configuration.rb
def method_missing(method, *args)
if method =~ /=$/
@configurations[$`.to_sym] = args.first
else
@configurations.fetch(method) {
@configurations[method] = ActiveSupport::OrderedOptions.new
}
end
end
Only ActiveSupport::OrderedOptions and a bit of metaprogramming.
Another perk of Active Support is try
method.
7 def try(*a, &b)
8 try!(*a, &b) if a.empty? || respond_to?(a.first)
9 end
10
11 def try!(*a, &b)
12 if a.empty? && block_given?
13 if b.arity == 0
14 instance_eval(&b)
15 else
16 yield self
17 end
18 else
19 public_send(*a, &b)
20 end
21 end
There is nothing difficult about that, either. For any of you, just like me, who were unaware that arity
determines the quantitative and qualitative set of arguments that the method returns:
class C
def one; end
def two(a); end
def three(*a); end
def four(a, b); end
def five(a, b, *c); end
def six(a, b, *c, &d); end
end
c = C.new
c.method(:one).arity #=> 0
c.method(:two).arity #=> 1
c.method(:three).arity #=> -1
c.method(:four).arity #=> 2
c.method(:five).arity #=> -3
c.method(:six).arity #=> -3
# ©https://apidock.com/ruby/Method/arity
Another common example from Active Support is the in?
predicate. predicate. And yes, Ruby can only respond to include?
. Also Ruby does not know such a method as the parent of the module / class. In any confusing situation it will return to Object
.
13 parent_name = name =~ /::[^:]+\Z/ ? $```.freeze : nil
...
35 parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
And Ruby does not know about subclasses (subclasses method). To get this information, you have to turn upside down the entire ObjectSpace
and that is a lot of resources:
#irb
> ObjectSpace.each_object(Class){|k| p k.name}
.....
=> 510
#rails c
> ObjectSpace.each_object(Class){|k| p k.name}
..........................(Beware – a very long output)
=> 15795
Did you know that in Rails, you can define the attributes of a class (not instance)? What ’s more, you can do it, not through the classic idiom “@@”, but through the wonderful class_attribute
. Yes, and redefine them later in the instance. Compare:
#ruby
@@default_params = {mime_version: "1.0", charset: "UTF-8", content_type: "text/plain", parts_order: [ "text/plain", "text/enriched", "text/html" ]}.freeze
and:
class_attribute :default_params
self.default_params = {mime_version: "1.0",
charset: "UTF-8",
content_type: "text/plain",
parts_order: [ "text/plain", "text/enriched", "text/html" ]}.freeze
The difference is significant. Not only do we affect default_params
from the instance as we want. But also we can still inherit from the base class and set default_params
in the derived class without damaging the base class.
#irb
> class TestClass
> @@variable = "var"
> def self.variable
> # Return the value of this variable
> @@variable
> end
> end
=> :variable
> TestClass.variable
=> "var"
> class AnotherClass < TestClass
> @@variable = "not var"
> def self.variable
> # Return the value of this variable
> @@variable
> end
> end
=> :variable
> AnotherClass.variable
=> "not var"
> TestClass.variable
=> "not var"
With class_attribute
this won’t happen.
Also, instance_values
, instance_variable_names
methods are not present in the base ruby libraries - these wraps over instance_variables
(Object
method) are defined in the extension.
The module Date
offers huge opportunities. Here you can find the nearest dates by the day of the week, and the date before or after the appointed date at a given interval.
Not to mention friendship Date
and DateTime
.
> date = Date.new(2018, 4, 30)
=> Mon, 30 Apr 2018
> date.beginning_of_day
=> Mon, 30 Apr 2018 00:00:00 WIB +07:00
By the way, in irb the line date = Date.new (2018, 4, 30)
will have a completely different output.
> date = Date.new(2018, 4, 30)
=> #<Date: 2018-04-30 ((2458239j,0s,0n),+0s,2299161j)>
This is also Active Support, and, specifically, the redefinition of the inspect
and to_s
methods in the Date
class.
# activesupport/lib/active_support/core_ext/date/conversions.rb
46 def to_formatted_s(format = :default)
47 if formatter = DATE_FORMATS[format]
48 if formatter.respond_to?(:call)
49 formatter.call(self).to_s
50 else
51 strftime(formatter)
52 end
53 else
54 to_default_s
55 end
56 end
57 alias_method :to_default_s, :to_s
58 alias_method :to_s, :to_formatted_xs
...
61 def readable_inspect
62 strftime("%a, %d %b %Y")
63 end
64 alias_method :default_inspect, :inspect
65 alias_method :inspect, :readable_inspect
And it’s only the tip of the iceberg
Of course, you should not say that Ruby does not know anything about “safe” sting. You wouldn't find truncate
, starts_with?
and ends_with?
, indent
methods in base libraries. Unable to find all cool stuff like dasherize
, titleize
, underscore
, camelize
, demodulize
and etc. Would you like to process HTTP request parameters? Use Active Support (methods to_param
, to_query
). Can’t access to the elements of the objects collection with from
,to
. There are extensions for Numeric
classes (years
, days
, hours
, seconds
, kilobytes
, megabytes
, gigabytes
,exabytes
) and many other more effective supplements.
Learn more about Active Support advantages here.