Ruby Features
Ruby Features makes the extending of Ruby classes and modules to be easy, safe and controlled.
Why?
Lets ask, is 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 question is about motivation to write such things. Lets skip well-known reason 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 any 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;
- Any moment gives the knowledge 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 ends 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
instance_methods do
# instance methods
def useful_instance_method
end
end
class_methods do
# class methods
def useful_class_method
end
end
apply_to 'ActiveRecord::Relation' do
# feature can contain several apply_to definition
end
end
end
Dependencies
The dependencies from other Ruby Features can be defined like:
RubyFeatures.define 'main_feature' do
dependency 'dependent_feature1'
dependencies 'dependent_feature2', 'dependent_feature3'
end
Conditions
Sometimes is required to apply different things, dependent 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 cause not predictable 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 feature definition helpers supports 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.('../lib/features', __FILE__))
Feature applying
Feature can be applied immediately after it's definition:
RubyFeatures.define 'some_namespace/something_useful' do
# definition
end.apply
Features can be applied right after loading from path:
RubyFeatures.find_in_path(File.('../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.1.0
- Added conditions.
- Added dependencies.
License
MIT License. Copyright (c) 2015 Sergey Tokarenko