Refinuri has two primary functions related to querying and filtering data:
- a simple way to produce pretty, meaningful URLs, even with complex query strings
- a standardized, extensible interface to filtering metadata
Refactor an ugly 'traditional' URL, such as
craigslist.org/search/housing?cat=loft&minPrice=500&maxPrice=1000&cats=true&dogs=true&bedrooms=2
to
craigslist.org/search/housing/type:loft;price:500-1000;pets:cats,dogs;bedrooms:2/
And parse it into a convenient set of filters
@filters = Refinuri::Parser.parse_url(params[:filters])
Now you can pass all that filtery goodness straight into your ActiveRecord object
Apartment.filtered(@filters)
# which can be scoped even further just like normal
Apartment.filtered(@filters).order('bedrooms DESC').limit(20).includes(:amenities)
And easily add links to your views to create, update, or delete the filters
filter_with_link("Allow Ferrets", { :pets => 'ferrets' })
filter_with_link("Only cats", { :create => { :pets => 'cats' } })
filter_with_link("Any Price", { :delete => { :price => nil } })
Not only does Refinuri improve the readability and length of URLs, but behind the scenes each piece of filtering data is being handled as the appropriate datatype automatically. A range of prices is an actual Range, a list of items is actually an Array, etc.
This is useful both because it allows you to hand the queries off to ActiveRecord very easily, and also because the data can be changed very easily. Individuals filters can accept arbitrary numbers of items, numeric filters and go from Integers to Ranges with no effort, etc.
The hope is that, with most of the heavy lifting being done automatically and in a consistent way, it will be much easier to actually implement a user-facing interface that allows for more advanced and useful controls to be implemented.
$ sudo gem install refinuri
match 'products/:id/:filters' => 'products#index'
before_filter :refine_filters
private
def refine_filters
@filters = Refinuri::Parser.parse_url(params[:filters]) if params[:filters]
end
@products = Product.filtered(@filters)
<%= filter_with_link("Price less than $50", { :price => '..50' }) %>
<% @products.each do |product| %>
<%= product.name %>
<% end %>
Filters are initially defined as a Hash, where each key represents an attribute upon which a resource can be filtered, and the value represents the values that will pass the filter.
The following hash creates three filters:
{ :name => ['apple','banana','cherry'], :price => 0..5, :weight => '4..' }
The Hash is used to create a new FilterSet, the class responsible for handling changes to the filters and outputting the filter values appropriately when necessary.
Refinuri::Base::FilterSet.new({ :name => ['apple','banana','cherry'], :price => 0..5, :weight => '4..' })
The FilterSet uses #merge! to accept changes to an existing set of filters. #merge! also accepts a hash, but allows for defining how the filters should be merged into the set.
By incorporating the various CRUD functions into the Hash, the new filters and values will be merged into the existing set.
@filters.merge!({ :create => { :color => 'orange' }, :update => { :name => 'dewberry', :price => 0..10 }, :delete => { :weight => nil } })
Any filters being merged that aren't explicitly stated as one of the CRUD functions is assumed to be an :update.
#to_h returns the entire set as a hash, similar to the one used to initialize the filter, but with any changes that have been made
@filters.to_h
=> { :name => ['apple','banana','cherry','dewberry'], :color => ['orange'], :price => 0..10, :weight => '4..' }
#to_url returns the entire set as a URL-friendly string, which could be included as an option in a link_to() helper
@fitlers.to_url
=> "name:apple,banana,cherry,dewberry;color:orange;price:0-10;weight:4+"
#value returns the filter value as it was defined
@filters[:name].value
=> ['apple','banana','cherry']
#to_db returns a value suitable for passing to a #where chain of an ActiveRecord object
@filters[:name].to_db
=> { :name => ['apple','banana','cherry'] }
#to_s returns a value suitable for use in a URL
@filters[:name].to_s
=> "apple,banana,cherry"
Filters defined within strings, such as those contained in a URL, can be parsed into a FilterSet using:
Refinuri::Parser.parse_url()
There are two ways of using FilterSets and filters with ActiveRecord.
The first is to utilize an individual filter manually along side the #where method, which is part of the Rails 3 query interface. The #to_db method returns values for filters specifically designed for use as a #where argument.
Product.where(@filters[:price].to_db) # => like Product.where(:price => 0..10)
Product.where(@filters[:name].to_db) # => like Product.where(:name => ['apple','banana','cherry'])
Product.where(@filters[:weight].to_db) # => like Product.where('weight <= 10')
The second is to pass the entire FilterSet off to ActiveRecord, and filter the object using all currently defined filters. The #filtered method is provided to ActiveRecord::Base for this purpose.
Product.filtered(@filters)
This is like doing
Product.where(@filters[:price].to_db).where(@filters[:name].to_db).where(@filters[:weight].to_db)
There are several link helpers included to serve common needs of creating and updating filters through the UI.
The first is a simple interface for passing a set of changes to the current set of filters, and returning the appropriate link.
filter_with_link("+ apple", { :name => 'apple' })
filter_with_link("- apple", { :delete => { :name => 'apple' } })
The next acts as a toggle for values within array-based filters, and will toggle individual values (not the entire filter, per se)
toggle_filter_with_link("+/- apple", { :name => 'apple' })
In URLs an array is represented as
key:item1,item2,item3
Which are parsed as standard Ruby arrays, available through the FilterSet
@set[:key] => ['item1','item2','item3']
In URLs a range is represented as
key:10-20
Which are parsed as standard Ruby ranges, available through the FilterSet
@set[:key] => 10..20
(Support for both inclusive and exclusive ranges is not yet included, but coming soon)
Unbounded ranges are a non-ruby-standard datatype, which allows for a convenient way of defining only an upper- or lower-bound on a range.
Represented in URLs as
key:10+
Parsed into the filter as strings in the format
@set[:key] => '10..'
And provides a convenience method for use in an ActiveRecord #where
@set[:key].to_db => 'key >= 10'
Represented in URLs as
key:10-
Are parsed into strings in the format
@set[:key] => '..10'
And provides a convenience method for use in an ActiveRecord #where
@set[:key].to_db => 'key <= 10'