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?, indentmethods 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.


Dmitry
Radionov

Backend Developer at JetRockets

Explore more of JetRockets