paraphrase
Paraphrase provides a way to map query params to model scopes and only apply scopes when the mapped query params are present.
Installation
Via a Gemfile:
gem 'paraphrase'
Or manually:
$ gem install paraphrase
Usage
Subclass Paraphrase::Query and use map to define what query params should
be applied to which scopes.
# app/queries/post_query.rb
class PostQuery < Paraphrase::Query
map :author, to: :by_user
map :start_date, :end_date, to: :published_within
end
By default, the ActiveRecord class is introspected from the demodulized class
name of the Paraphrase::Query sublcass. If the name of the query class is
not <model>Query, the source can be manually specified by passing a string or
symbol to the source method.
# app/queries/admin_post_query.rb
class AdminPostQuery < Paraphrase::Query
# This needs the source specific since it will look for an `AdminPost` model.
self.source = :Post
end
To build the query, call .paraphrase on your model. Only scopes whose keys are all
provided will be applied.
# Based on the example `PostQuery` above, this will only apply `Post.by_user`
# and skip `Post.published_within` since `:end_date` is missing.
Post.paraphrase(author: 'Jim')
All unregistered keys are filered out of the params that are passed to .paraphrase.
class PostsController < ApplicationController
respond_to :html, :json
def index
# Will filter out keys such as `:action` and `:controller`
@posts = Post.paraphrase(params)
respond_with(@posts)
end
end
Paraphrase::Query will recursively determine if the value of the query
param is empty. If the value is an array containing empty strings, the empty
strings will be removed before being passed to the scope. If the array is empty
after removing empty strings, the scope will not be called since an empty array
is considered a blank value.
class UserQuery < Paraphrase::Query
map :names, to: :with_name
end
class User < ActiveRecord::Base
def self.with_name(names)
where(name: names)
end
end
User.paraphrase(names: ['', 'Jim']).to_sql
# => SELECT "users".* FROM "users" WHERE "users"."name" IN ['Jim']
User.paraphrase(names: ['', '']).to_sql
# => SELECT "users".* FROM "users"
You can chain queries on an ActiveRecord::Relation. This avoids adding scopes
that replicate the functionality of an association like
Post.for_user(user_id) or allow you to build a default scope.
class PostsController < ApplicationController
respond_to :html, :json
# GET /users/:id/posts
def index
@user = User.find(params[:user_id])
# This will scope the query to posts where `posts`.`user_id` = `users`.`id`
@posts = @users.posts.paraphrase(params[:q])
# Or you can build at a different point in a scope chain
# @posts = @user.posts.published.paraphrase(params[:q])
#
# Order is independent too
# @posts = @user.posts.paraphrase(params[:q]).published
respond_with(@posts)
end
end
Query Class DSL
Scopes are mapped to param keys using map. You can specify one or more keys.
class PostQuery < Paraphrase::Query
map :first_name, :last_name, to: :by_user
map :pub_date, to: :pub_date
end
class Post < ActiveRecord::Base
def self.by_user(first_name, last_name)
joins(:user).where(user: { first_name: first_name, last_name: last_name })
end
def self.published_on(pub_date)
where(published_on: pub_date)
end
end
If multiple query params are mapped to a scope, but only a subset are required,
use the :whitelist option to allow them to be blank. The :whitelist
option can be set to true, an individual key or an array of keys.
class PostQuery < Paraphrase::Query
# requires only :last_name to be passed in, :first_name can be nil
map :first_name, :last_name, to: :by_author, whitelist: :last_name
end
class Post < ActiveRecord::Base
def self.(first_name, last_name)
query = where(user: { first_name: first_name })
if last_name
query = query.where(user: { last_name: last_name })
end
query
end
end
Post.paraphrase(first_name: 'John').to_sql
# => SELECT "posts".* FROM "posts" WHERE "posts"."first_name" = 'John'
Post.paraphrase(first_name: 'John', last_name: 'Smith').to_sql
# => SELECT "posts".* FROM "posts" WHERE "posts"."first_name" = 'John' AND "posts"."last_name" = 'Smith'
Boolean Scopes
For scopes that filter records based on a boolean column, it doesn't make to force the scope to take an argument.
If the mapped query params are present and a scope takes no arguments,
paraphrase will not attempt to pass those values to the query.
class PostQuery < Paraphrase::Query
map :published, to: :published
end
class Post < ActiveRecord::Base
# If the params supplied include a non-empty value for :published, this
# method will be called.
def self.published
where('published_at IS NOT NULL')
end
end
Post.paraphrase(published: '1').to_sql
# => SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't'
Pre-processing Query Params
To pre-process a query param, such as an ISO formatted date, you can either use
the param class method or re-open the ParamsFilter class that is defined
when inheriting from Paraphrase::Query. Using the param class method
defines the equivalent method on the ParamsFilter class.
In the method, you have access to the params attribute that represents the
original, unprocessed params.
class PostQuery < Paraphrase::Query
map :start_date, :end_date, to: :published_within
class ParamsFilter
def start_date
Time.zone.parse(params[:start_date]) rescue nil
end
end
param :end_date do
Time.zone.parse(params[:end_date]) rescue nil
end
end
class Post < ActiveRecord::Base
def self.published_within(start_date, end_date)
where(published_at: start_date..end_date)
end
end
Post.parahrase(start_date: '201-03-21', end_date: '2013-03-25').to_sql
# => SELECT "posts".* FROM "posts"
In the above example, if either :start_date or :end_date are incorrectly
formatted, the pubished_within scope will not be applied because the values
are will be nil.
Define scopes on the Query class
If your model is cluttered with scopes that aren't general-purpose, and only
used by your query class, you can define them in the query class. You can
define scopes by re-opening the Repository class defined on inheritance from
Paraphrase::Query. There is also the scope class method that serves as a
proxy for defining methods on the Repository class.
In the method, you have to call the scope on the relation property of the
Repository instance.
class PostQuery < Paraphrase::Query
map :title, to: :titled
map :authors, to: :by_users
map :is_published, to: :published
scope :by_users do ||
relation.joins(:user).where(users: { name: })
end
# OR
# class Repository
# def by_users(authors)
# relation.joins(:user).where(users: { name: authors })
# end
# end
end
class Post < ActiveRecord::Base
end
Post.paraphrase(authors: ['Robert', 'Susie']).to_sql
# => SELECT "posts".* FROM "posts"
# INNER JOIN "users" ON "users"."id" = "posts"."user_id"
# WHERE "users"."name" IN ('Robert', 'Susie')
Using with FormBuilder
The Paraphrase::Query class implements the ActiveModel API required for use
with form builders.
class PostQuery < Paraphrase::Query
map :author, to: :by_user
end
class PostsController < ApplicationController
def index
@query = PostQuery.new(params[:q])
@posts = query.result
end
end
<%= form_for @query, url: posts_url, method: :get do |f| %>
<%= f.label :author %>
<%= f.select :author, options_from_collection_for_select(User.authors, :id, :name) %>
<% end %>
<% @posts.each do |post| %>
...
<% end %>
Contributing
Contributions welcome. Be sure to include tests for any regressions or features.
- Fork it ( http://github.com/[my-github-username]/paraphrase/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature with tests') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request

