Fluent interface in Ruby
Let’s imagine that you want to write your implementation of Rails where
method. What for you need it? For example, if you have old RoR 2 project and you have no any wish to write where statements in pure SQL. What are the requirements of such service? You should have ability to chain where
statements and transform all this into conditions for Rails 2 ActiveRecord.
You can see example of Ruby on Rails 2 ActiveRecord here. Example:
Client.first(:conditions =>
["orders_count = ? AND locked = ?", params[:orders], false])
What can help us? Fluent interface. The main idea of this pattern is to return operating object on each method call.
For out implementation we will use two arrays, conditions
and arguments
to store corrseponding values. Additionaly we need a grammatical conjunction keyword
to concatenate statements.
def initialize(keyword = "AND")
@keyword = keyword
@conditions = []
@arguments = []
end
Then we need where
method that will collect our conditions. As I said before there is no any magick, just store fields, arguments and return self.
def where(condition, argument)
return self if argument.nil?
@conditions << "#{condition} = (?)"
@arguments << argument
self
end
When we have all this we need to transform conditions into RoR Array Condition. We will join our arguments with keyword.
def prepare
return [] if @conditions.empty?
[contatinate_statements, *@arguments]
end
private
def contatinate_statements
@conditions.join(" #{@keyword} ")
end
And that’s all. Full code
class CollectionFilteringService
def initialize(keyword = "AND")
@keyword = keyword
@conditions = []
@arguments = []
end
def where(condition, argument)
@conditions << "#{condition} = (?)"
@arguments << argument
self
end
def prepare
return [] if @conditions.empty?
[contatinate_statements, *@arguments]
end
private
def contatinate_statements
@conditions.join(" #{@keyword} ")
end
end
And example of how it works.
conditions = CollectionFilteringService
.where(:field1, :value1)
.where(:field2, :value2)
.prepare
Client.first(:conditions => conditions)
You can easily add any kind of method that you want. For example if you are using MicrosoftSQL and you need to receive fields by date even if it stored in datetime you can realize your #where_date
method. As MicrosoftSQL make some internal magick with dates escaping we will use a little trick and pass arguments directly:
def where_date(field, argument)
@conditions << "datediff(day, #{field}, '#{argument}') = 0"
self
end
And use it. If you have a lot of patience, with this technic in mind you can reailise full analog of ActiveRecord. And don’t forget to cover all this with tests.
require 'minitest/autorun'
require_relative 'collection_filtering_service'
describe CollectionFilteringService do
subject { CollectionFilteringService.new }
it "transform where condition into SQL query" do
assert_equal subject.where(:field1, :value).prepare,
["field1 = (?)", :value]
end
it "transform where_date condition into SQL query" do
assert_equal subject.where_date(:datefield, :value).prepare,
["datediff(day, datefield, 'value') = 0"]
end
it "allows to chain `where` statements" do
assert_equal subject.where(:field1, :value1).where(:field2, :value2).prepare,
["field1 = (?) AND field2 = (?)", :value1, :value2]
end
it "allows to chain `where_date` statements" do
assert_equal subject.where(:field1, :value1).where_date(:datefield, :value).prepare,
["field1 = (?) AND datediff(day, datefield, 'value') = 0", :value1]
end
it "return empty array for query withoud conditions" do
assert subject.prepare.empty?
end
end