Helps clean up complex URLs with filtering query string, like you may find in an online store
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.gitignore
LICENSE
README.md
Rakefile
TODO
VERSION
refinuri-0.5.1.gem
refinuri-0.5.2.gem
refinuri.gemspec

README.md

Refinuri

ri-fahy-nuh-ree

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

The dime tour

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 } })

Benefits

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.

Usage

Installation

$ sudo gem install refinuri

Example Application

routes.rb

match 'products/:id/:filters' => 'products#index'

application_controller.rb

before_filter :refine_filters

private
def refine_filters
    @filters = Refinuri::Parser.parse_url(params[:filters]) if params[:filters]
end

products_controller.rb

@products = Product.filtered(@filters)

products/index.html.erb

<%= filter_with_link("Price less than $50", { :price => '..50' }) %>
<% @products.each do |product| %>
    <%= product.name %>
<% end %>

API

Defining filters

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..' })

Modifying filters

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.

Returning values

Values of entire filter sets:

#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+"

Values of individual filters within a set:

#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"

Parsing filter strings

Filters defined within strings, such as those contained in a URL, can be parsed into a FilterSet using:

Refinuri::Parser.parse_url()

ActiveRecord integration

There are two ways of using FilterSets and filters with ActiveRecord.

Individually

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')

En masse

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)

ActionView helpers

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' })

Data types

Arrays

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']

Ranges

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

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.

Lower-bound ranges

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'

Upper-bound ranges

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'