Advanced

Advanced is a library for building complex searches with Active Record.

Click here to see the source for the demo application.

Installation

Add this line to your application's Gemfile:

gem 'advanced'

And then execute:

$ bundle

Or install it yourself as:

$ gem install advanced

Usage

Start by inheriting Advanced::Search.

class StateSearch < Advanced::Search
  def search_name(name:, **)
    where('states.name like ?', "%#{name}%")
  end
end

Advanced::Search will look at the methods you've defined that start with search_. Above, we've declared the search_name method and listed the name parameter as a required key.

StateSearch.call(State.all, name: 'New')
#=> SELECT * FROM states WHERE states.name like '%New%'

StateSearch.call(State.all, foo: 'bar')
#=> SELECT * FROM states

StateSearch.call(State.all, name: '')
#=> SELECT * FROM states

Search composition

We can compose multiple search objects.

class CitySearch < Advanced::Search
  def search_name(name:, **)
    where('cities.name like ?', "%#{name}%")
  end

  def search_state(state:, **)
    joins(:state).merge StateSearch.call(State.all, state)
  end
end
CitySearch.call(City.all, name: 'New York')
#=> SELECT  "cities".* FROM "cities" WHERE (cities.name like '%New York%')

CitySearch.call(City.all, state: { name: 'New York' })
#=> SELECT  "cities".* FROM "cities"
#=> INNER JOIN "states" ON "states"."id" = "cities"."state_id"
#=> WHERE (states.name like '%New York%')

Advanced::SearchForm

A common feature in Rails applications is building filters. Advanced::SearchForm can help you do that. It's basically just a hash that is compatible with Rails form builders.

class StateSearch < Advanced::Search
  # ...

  class Form < Advanced::SearchForm
    search StateSearch
  end
end

It scans the keyword arguments in your search methods and defines accessors automatically:

form = StateSearch::Form.new(name: 'Foo')
form.name #=> 'Foo'
form.name = 'Bar'
form.name #=> 'Bar'
form.to_h #=> { name: 'Foo' }

Advanced::Search treats this object like a hash, so you can just pass it along:

StateSearch.call(State.all, form)

So, a controller action might look like this:

def index
  @form   = StateSearch::Form.new(params[:q])
  @states = StateSearch.call(State.all, @form)
end

Your view might look something like this:

<%= form_for @form, method: :get, url: states_path, as: :q do |f| %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </p>

  <%= f.submit 'Search' %>
<% end %>

<table>
  <thead>
    <tr>
      <th>Name</th>
    </tr>
  </thead>

  <tbody>
    <% @states.each do |state| %>
      <tr>
        <td><%= state.name %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Composing forms

You can compose forms just like your compose your searches.

class CitySearch < Advanced::Search
  # ...
  class Form < Advanced::SearchForm
    search CitySearch
    nested :state, StateSearch::Form
  end
end
form = CitySearch::Form.new(name: 'New York', state: { name: 'New York' })
form.name #=> 'New York'
form.state.name #=> 'New York'
form.to_h #=> { name: 'New York', state: { name: 'New York' }}

Then, in your view, you can use fields_for to generate your form fields:

<%= form_for @form, method: :get, url: cities_path, as: :q do |f| %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </p>

  <%= f.fields_for :state do |state| %>
    <p>
      <%= state.label :name, 'State' %>
      <%= state.text_field :name %>
    </p>
  <% end %>

  <%= f.submit 'Search' %>
<% end %>

Other goodies

You can extend your models with a search method to keep it brief.

class State < ApplicationRecord
  extend StateSearch.scope
end

Now, you have a shorthand:

State.search(name: 'New York')

If you'd prefer to call your search method something other than search, that's cool too:

extend StateSearch.scope(:custom_search)

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Ray Zane/advanced.

License

The gem is available as open source under the terms of the MIT License.