Findit

Tired of writing fat controllers? But you must do all these queries.. There is a solution, move it to a special Finder class!

Stop writing this:

class SomeController
  def index
    @cache_key = (
      # many_values
    )
    return if fragment_exists?(@cache_key)
    search_scope = SearchEngine.scope
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    search_scope.add(some_conditions)
    result = search_scope.search_and_return_ids
    @scope = scope.where(ids: result)
    ....
  end
end

Just do this:

# /app/controllers/some_controller.rb
class SomeController
  def index
    @scope = SomeFinder.new(params)
  end
end

# app/finders/some_finder.rb
class SomeFinder
  include Findit::Collections

  cache_key do
    # calculate you cache_key from params
  end

  def initialize(params)
    # some initialize, maybe params parse
  end

  private

  def find
    # put here you find logic
  end
end

And that it! Now you can iterate over finder results by simple each:

@scope = SomeFinder.new(params)
@scope.each do |d|
  print d.description
end

or perform caching like ActiveRecord::Base

# app/some_view
<% cache @scope do %>
  <%scope.each do |res|%>
    ...
  <%end%>
<%end%>

Installation

Add this line to your application's Gemfile:

gem 'findit'

And then execute:

$ bundle

Or install it yourself as:

$ gem install findit

Per module documentation

Collections

It makes Finder work as Enumerator with lazy load. Result can be accessed with each, [] and size methods, but to make things work you must implement find method.

  class PostFinder
    incliude Findit::Collections

    private # make it private, so no one call it without lazy load

    def find
      Post.where(user_id: 1)
    end
  end

  @posts = PostFinder.new

  # load all matching posts and iterate over collection
  @posts.each do |post|
    print post.title
  end

  @posts[10] # get 10 element of posts

  @posts.size # size of PostFinder results

  # Also you can access result direcly by using `data` method.
  @posts.data # access to all posts

For easier caching expirience we provide DSL to define you custom cache_key

#/app/finders/posts_finders.rb
class PostFinder
  include Findit::Collections

  cache_key do
    [@user.id, @query] # here you put any stuff that result of finder depend on it
  end

  # custom initializer, do whatever you want here
  def initialize(options = {})
    @user = options.fetch(:user)
    @query = options[:query]
  end

  private
  # Here we fetch results. You MUST implement it
  def find
    scope = scope.where(user_id: @user.id)
    scope = scope.where('description like :query', query: @query) if @query.present?
    scope
  end
end

#/app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = PostFinder.new(user: current_user)
  end
end

#/app/views/posts/index.html.erb
<% cache(@posts, expire_in: 30.minutes) do %>
   <%=render 'post', collection: @posts, as: :post%> # it will automaticly iterate over finder results by each method

WillPaginate

It adds delegation of will_paginate methods to finder.

Example usage:

# app/finders/post_finder.rb
class PostFinder
  include Findit::Collection
  include Findit::WillPaginate

  cache_key do
    [@page, @per_page]
  end

  def initialize(page, per_page)
    @page = page
    @per_page = per_page
  end

  private

  def find
    scope = Post.paginate(per_page: per_page, page: page)
  end
end

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
    @posts = PostFinder.new(params[:page], params[:per_page])
  end
end

# app/views/posts/index.html.erb
<% cache(@posts, expire_in: 30.minutes) do %>
  <%= render 'post', collection: @posts, as: :post %>
  <%= will_paginate @posts %>

Single

Adds DSL for cache_key on Finder with single element to find.

Example usage:

# app/finders/post_finder.rb
class PostsFinder
  include Findit::Single

  cache_key do
    @user
  end

  def initialize(user)
    @user = user
  end

  private

  def find
    Post.where(user: user).last
  end
end

Cache

Extends finder with cache possibility. Every call of call method will be cached in Rails.cache. Method cache options allows you to add custom options like expire_in or tags to Rails.cache.fetch. If you want to disable cache dependent of initialization arguments, you can use cache? DSL method.

All in one Example:

# app/finders/post_finder.rb
class CachedPostsFinder
  include Findit::Single
  include Findit::Cache

  cache_key do
    @user
  end

  cache_options do
    {expire_in: 15.minutes} # This will be directly passed to Rails.cache.fetch
  end

  def initialize(user)
    @user = user
  end

  private

  def find
    Post.where(user: user)
  end
end

To disable cache for some reasone you can call special method without_cache:

CachedFinder.new(user).without_cache.load # no cache

CachedFinder.new(user).load - # will perform cache operations

If you want this functionality on Collections finder you can add extension Findit::Collections alongside with Cache:

class SomeFinder
  include Findit::Collections
  include Findit::Cache

  ...
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/abak-press/findit.