shuber/queryable_array

View on GitHub
README.rdoc

Summary

Maintainability
Test Coverage
= 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