README.rdoc
= queryable_array - {<img src="https://secure.travis-ci.org/shuber/queryable_array.png"/>}[http://travis-ci.org/shuber/queryable_array] {<img src="https://codeclimate.com/github/shuber/queryable_array/badges/gpa.svg" />}[https://codeclimate.com/github/shuber/queryable_array] {<img src="https://codeclimate.com/github/shuber/queryable_array/badges/coverage.svg" />}[https://codeclimate.com/github/shuber/queryable_array]
A +QueryableArray+ inherits from +Array+ and is intended to store a group of
objects which share the same attributes allowing them to be searched. It
overrides <tt>[]</tt>, +find_all+ and +method_missing+ to provide a simplified DSL
for looking up objects by querying their attributes.
View the full documentation over at rubydoc.info[http://rubydoc.info/github/shuber/queryable_array/frames].
== Installation
gem install queryable_array
== Requirements
Ruby 1.9+
== Usage
=== Basic
Initialize the +QueryableArray+ with a collection of objects e.g. +Page+ objects from a JSON response or database query (although you should probably restrict database queries with WHERE conditions instead if you have the opportunity)
pages = QueryableArray.new Page.all
The +pages+ object can then be queried by passing a search hash to the <tt>[]</tt> method
pages[uri: '/'] # => #<Page @uri='/' @name='Home'>
pages[name: 'About'] # => #<Page @uri='/about' @name='About'>
pages[uri: '/', name: 'Home'] # => #<Page @uri='/' @name='Home'>
pages[uri: '/', name: 'Mismatch'] # => nil
Notice that it only returns the first matching object or +nil+ if one is not found. If you'd like to find
all matching objects, simply wrap your search hash in an array
pages[[published: true]] # => [#<Page @uri='/' @name='Home' @published=true>, #<Page @uri='/about' @name='About' @published=true>, ...]
pages[[uri: '/missing']] # => []
Attributes may also be searched by regular expressions
pages[name: /home/i] # => #<Page @uri='/' @name='Home'>
pages[[uri: /users/]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
The methods +find_by+ and +find_all+ behave as aliases for <tt>[search_hash]</tt> and <tt>[[search_hash]]</tt> respectively
pages.find_by(name: 'Home') # => #<Page @uri='/' @name='Home'>
pages.find_by(name: 'Missing') # => nil
pages.find_all(uri: /users/) # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
The existing block form for those methods work as well
pages.find_all { |page| page.uri =~ /users/ } # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
A +Proc+ object may be passed to <tt>[]</tt> as well
pages[uri: proc { |uri| uri.split('/').size > 1 }] # => #<Page @uri='/users/bob' @name='Bob'>
pages[proc { |page| page.uri == '/' }] # => #<Page @uri='/' @name='Home'>
Lookups by index or ranges still behave exactly as they do in regular +Array+ objects
pages[0] # => #<Page @uri='/' @name='Home'>
pages[-1] # => #<Page @uri='/zebras' @name='Zebras'>
pages[99] # => nil
pages[0..1] # => [#<Page @uri='/' @name='Home'>, #<Page @uri='/about' @name='About'>]
=== Default finders
A +QueryableArray+ object can be initialized with a +default_finder+ to make lookups even simpler
pages = QueryableArray.new Page.all, :uri
Now the +pages+ object can be searched easily by +uri+
pages['/'] # => #<Page @uri='/' @name='Home'>
pages['/about'] # => #<Page @uri='/about' @name='About'>
pages['/missing'] # => nil
You can even specify multiple +default_finders+
pages = QueryableArray.new Page.all, [:uri, :name]
pages['/about'] # => #<Page @uri='/about' @name='About'>
pages['About'] # => #<Page @uri='/about' @name='About'>
pages[/home/i] # => #<Page @uri='/' @name='Home'>
Wrapping your search inside an array still returns all matches
pages[[/users/]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
=== Dynamic attribute-based finders
<tt>QueryableArray#method_missing</tt> allows you to lookup objects using a notation like the +ActiveRecord+ dynamic finders
pages.find_by_uri('/') # => #<Page @uri='/' @name='Home'>
pages.find_by_uri_and_name('/', 'Home') # => #<Page @uri='/' @name='Home'>
pages.find_by_uri('/missing') # => nil
pages.find_all_by_uri('/') # => [#<Page @uri='/' @name='Home'>]
pages.find_all_by_uri(/users/) # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
=== Dot notation finders
If any +default_finders+ are defined you may even use dot notation to lookup objects by those attributes
pages = QueryableArray.new Page.all, :name
pages.sitemap # => #<Page @uri='/sitemap' @name='Sitemap'>
pages.missing # => NoMethodError
QueryableArray.new.missing # => NoMethodError
Calling <tt>pages.sitemap</tt> behaves the same as <tt>pages[/sitemap/i]</tt>
To perform a case-sensitive search, simply append a <tt>!</tt> to the end of your method call
e.g. <tt>pages.sitemap!</tt> which calls <tt>pages['sitemap']</tt>
You may also query to see if a match exists by appending a <tt>?</tt> to your search
pages.sitemap? # => true
pages.missing? # => false
=== Composable
Functionality for +QueryableArray+ has been separated out into individual modules
containing their own features which allows you to create your own objects and only
include the features you care about
* <tt>QueryableArray::DefaultFinder</tt> - Allows objects to be searched by +default_finders+ thru <tt>[]</tt>
* <tt>QueryableArray::DotNotation</tt> - Allows objects to be searched using dot notation thru +method_missing+ which behaves like an alias to <tt>QueryableArray::DefaultFinder#[]</tt>
* <tt>QueryableArray::DynamicFinder</tt> - Allows objects to be searched by dynamic finders thru +method_missing+ similar to the ActiveRecord dynamic attribute-based finders e.g. +find_by_email+ or +find_all_by_last_name+
* <tt>QueryableArray::Queryable</tt> - Allows +find_by+ and +find_all+ to accept search hashes which are converted into +Proc+ searches and passed as the block arguments for +find+ and +find_all+ respectively
* <tt>QueryableArray::Shorthand</tt> - Makes <tt>[search_hash]</tt> and <tt>[[search_hash]]</tt> behave as an alias for +find_by+ and +find_all+ respectively
Try making your own classes with them
class Collection < Array
include QueryableArray::Queryable
end
pages = Collection.new Page.all
pages.find_all(published: true) # => [#<Page @uri='/' @published=true>, #<Page @uri='/about' @published=true>]
=== Real world example
Try using it inside of your templates:
<div class="posts">
<%- posts[[published: true]].each do |post| -%>
<div class="post">
<h2><a href="<%= post.url -%>"><%= post.title -%></a></h2>
<div class="excerpt"><%= post.excerpt -%></div>
<a href="<%= post.url -%>#comments"><%= post.comments[[approved: true]].size -%> comments</a>
</div>
<%- end -%>
</div>
== Testing
bundle exec rake