Ruby Features

Version Build Climate Coverage

Ruby Features allow the extending of Ruby classes and modules to become easy, safe and controlled.

Why?

Lets ask, is that good to write the code like this:

String.send :include, MyStringExtension

Or even like this:

Object.class_eval do
  def my_object_method
    # some super code
  end
end

The matter is in motivation to write such things. Lets skip the well-known reasons like

Because I can! That is Ruby baby, lets make some anarchy!

but say:

I want to implement the functionality, which I expected to find right in the box.

In fact, the cool things can be injected right in core programming entities in this way. They are able to improve the development speed, to make the sources to be more readable and light.

From the other side, the project's behavior loses the predictability once it requires the third-party library, infected by massive patches to core entities.

Ruby Features goal is to take that under control.

The main features are:

  • No dependencies;
  • Built-in lazy load;
  • Supports ActiveSupport lazy load as well;
  • Stimulates the clear extending, but prevents monkey patching;
  • Gives the control what core extensions to apply;
  • Gives the understading who and how exactly affected to programming entities.

requirements

  • Ruby >= 1.9.3

Getting started

Add to your Gemfile:

gem 'ruby-features'

Run the bundle command to install it.

For Rails projects, gun generator:

rails generate ruby_features:install

Generator will add ruby-features.rb initializer, which loads the ruby features from {Rails.root}/lib/features folder. Also such initializer is a good place to apply third-party features.

Usage

Feature definition

Feature file name should end with _feature.rb.

Lets define the feature in lib/features/something_useful_feature.rb:

RubyFeatures.define 'some_namespace/something_useful' do
  apply_to 'ActiveRecord::Base' do

    applied do
      # will be evaluated on target class
      attr_accessor :useful_variable
    end

    rewrite_instance_methods do
      # rewrite instance methods
      # call `super` to reach the rewritten method
      def existing_instance_method
        # some code before super
        super
        # some code after super
      end
    end

    instance_methods do
      # instance methods
      def useful_instance_method
      end
    end

    class_methods do
      # class methods
      def useful_class_method
      end
    end
  end

  apply_to 'ActiveRecord::Relation' do
    # feature can contain several apply_to definition
  end

end

Dependencies

The dependencies on other Ruby Features can be defined like:

RubyFeatures.define 'main_feature' do
  dependency 'dependent_feature1'
  dependencies 'dependent_feature2', 'dependent_feature3'
end

Conditions

Sometimes it`s required to apply different things, depending on some criteria:

RubyFeatures.define 'some_namespace/something_useful' do
  apply_to 'ActiveRecord::Base' do

    class_methods do
      if ActiveRecord::VERSION::MAJOR > 3
        def useful_method
          # Implementation for newest ActiveRecord
        end
      else
        def useful_method
          # Implementation for ActiveRecord 3
        end
      end
    end
  end
end

It's bad to do like that, because the mixin applied by Ruby Features became to be not static. That causes unpredictable behavior.

Ruby Features provides the conditions mechanism to avoid such problem:

RubyFeatures.define 'some_namespace/something_useful' do
  condition(:newest_activerecord){ ActiveRecord::VERSION::MAJOR > 3 }

  apply_to 'ActiveRecord::Base' do

    class_methods if: :newest_activerecord do
      def useful_method
        # Implementation for newest ActiveRecord
      end
    end

    class_methods unless: :newest_activerecord do
      def useful_method
        # Implementation for ActiveRecord 3
      end
    end

  end
end

All DSL methods support the conditions:

apply_to 'ActiveRecord::Base', if: :first_criteria do; end

applied if: :second_criteria do; end

class_methods if: :third_criteria do; end

instance_methods if: :fourth_criteria do; end

It's possible to define not boolean condition:

RubyFeatures.define 'some_namespace/something_useful' do
  condition(:activerecord_version){ ActiveRecord::VERSION::MAJOR }

  apply_to 'ActiveRecord::Base' do

    class_methods unless: {activerecord_version: 3} do
      def useful_method
        # Implementation for newest ActiveRecord
      end
    end

    class_methods if: {activerecord_version: 3} do
      def useful_method
        # Implementation for ActiveRecord 3
      end
    end

  end
end

It's possible to combine the conditions:

class_methods {
  if: [
    :boolean_condition,
    :other_boolean_condition,
    {
      string_condition: 'some_string',
      symbol_condition: :some_symbol
    }
  ],
  unless: :unless_boolean_condition
} do; end

Feature loading

Feature can be loaded by normal require call:

require `lib/features/something_useful_feature`

All features within path can be loaded as follows:

# require all "*_feature.rb" files within path, recursively:
RubyFeatures.find_in_path(File.expand_path('../lib/features', __FILE__))

Feature applying

Feature can be applied immediately after its definition:

RubyFeatures.define 'some_namespace/something_useful' do
  # definition
end.apply

Features can be applied immediately after loading from path:

RubyFeatures.find_in_path(File.expand_path('../lib/features', __FILE__)).apply_all

Feature can be applied by name, if such feature is already loaded:

require `lib/features/something_useful_feature`

RubyFeatures.apply 'some_namespace/something_useful'

Changes

v1.2.0

  • Added rewrite_instance_methods.

v1.1.0

  • Added conditions.
  • Added dependencies.

License

MIT License. Copyright (c) 2015 Sergey Tokarenko