Skip to content

Repository files navigation

Gem VersionCode coverage

SearchObject

DSL for building search objects.

Search objects start with an initial collection (scope) and allow it to be filtered based on various options.

Uses:

  • complicated search forms (example)
  • API endpoints with multiple filter conditions
  • GraphQL resolvers (example)
  • ... search objects 😀

Installation

Add this line to your application's Gemfile:

gem'search_object'

And then execute:

$ bundle

Or install it yourself as:

$ gem install search_object

Changelog

Changes are available in CHANGELOG.md

Usage

Just include the SearchObject.module and define your search options:

classPostSearchincludeSearchObject.modulescope{Post.all}option(:name){ |scope,value| scope.wherename: value}option(:created_at){ |scope,dates| scope.created_afterdates}option(:published,false){ |scope,value| value ? scope.unopened : scope.opened}end

Then you can just search the given scope:

search=PostSearch.new(filters: params[:filters])# accessing search optionssearch.name# => name optionsearch.created_at# => created at option# accessing resultssearch.count# => number of found resultssearch.results?# => is there any results foundsearch.results# => found results# params for url generationssearch.params# => option valuessearch.paramsopened: false# => overwrites the 'opened' option

Example

You can find example of most important features and plugins - here.

Plugins

SearchObject support plugins, which are passed to SearchObject.module method.

Plugins are just plain Ruby modules, which are included with SearchObject.module. They are located under SearchObject::Plugin module.

Paginate Plugin

Really simple paginate plugin, which uses the plain .limit and .offset methods.

classProductSearchincludeSearchObject.module(:paging)scope{Product.all}option:nameoption:category_name# per page defaults to 10per_page10# range of values is also possiblemin_per_page5max_per_page100endsearch=ProductSearch.new(filters: params[:filters],page: params[:page],per_page: params[:per_page])search.page# => page numbersearch.per_page# => per page (10)search.results# => paginated page results

Of course if you want more sophisticated pagination plugins you can use:

includeSearchObject.module(:will_paginate)includeSearchObject.module(:kaminari)

Enum Plugin

Gives you filter with pre-defined options.

classProductSearchincludeSearchObject.module(:enum)scope{Product.all}option:order,enum: %w(populardate)private# Gets called when order with 'popular' is givendefapply_order_with_popular(scope)scope.by_popularityend# Gets called when order with 'date' is givendefapply_order_with_date(scope)scope.by_dateend# (optional) Gets called when invalid enum is givendefhandle_invalid_order(scope,invalid_value)scopeendend

Model Plugin

Extends your search object with ActiveModel, so you can use it in Rails forms.

classProductSearchincludeSearchObject.module(:model)scope{Product.all}option:nameoption:category_nameend
<%# in some view: %><%= form_for ProductSearch.new do |form| %><% form.label :name %><% form.text_field :name %><% form.label :category_name %><% form.text_field :category_name %><% end %>

GraphQL Plugin

Installed as separate gem, it is designed to work with GraphQL:

gem 'search_object_graphql'
classPostResolverincludeSearchObject.module(:graphql)typePostTypescope{Post.all}option(:name,type: types.String){ |scope,value| scope.wherename: value}option(:published,type: types.Boolean){ |scope,value| value ? scope.published : scope.unpublished}end

Sorting Plugin

Fixing the pain of dealing with sorting attributes and directions.

classProductSearchincludeSearchObject.module(:sorting)scope{Product.all}sort_by:name,:priceendsearch=ProductSearch.new(filters: {sort: 'price desc'})search.results# => Product sorted my price DESCsearch.sort_attribute# => 'price'search.sort_direction# => 'desc'# Smart sort checkingsearch.sort?('price')# => truesearch.sort?('price desc')# => truesearch.sort?('price asc')# => false# Helpers for dealing with reversing sort directionsearch.reverted_sort_direction# => 'asc'search.sort_direction_for('price')# => 'asc'search.sort_direction_for('name')# => 'desc'# Params for sorting linkssearch.sort_params_for('name')

Tips & Tricks

Results Shortcut

Very often you will just need results of search:

ProductSearch.new(params).results == ProductSearch.results(params)

Passing Scope as Argument

classProductSearchincludeSearchObject.moduleend# first arguments is treated as scope (if no scope option is provided)search=ProductSearch.new(scope: Product.visible,filters: params[:f])search.results# => includes only visible products

Handling Nil Options

classProductSearchincludeSearchObject.modulescope{Product.all}# nil values returned from option blocks are ignoredoption(:sold){ |scope,value| scope.soldifvalue}end

Default Option Block

classProductSearchincludeSearchObject.modulescope{Product.all}option:name# automaticly applies => { |scope, value| scope.where name: value unless value.blank? }end

Using Instance Method in Option Blocks

classProductSearchincludeSearchObject.modulescope{Product.all}option(:date){ |scope,value| scope.by_dateparse_dates(value)}privatedefparse_dates(date_string)# some "magic" method to parse datesendend

Using Instance Method for Straight Dispatch

classProductSearchincludeSearchObject.modulescope{Product.all}option:date,with: :parse_datesprivatedefparse_dates(scope,value)# some "magic" method to parse datesendend

Active Record Is Not Required

classProductSearchincludeSearchObject.modulescope{RemoteEndpoint.fetch_product_as_hashes}option(:name){ |scope,value| scope.select{ |product| product[:name] == value}}option(:category){ |scope,value| scope.select{ |product| product[:category] == value}}end

Overwriting Methods

You can have fine grained scope, by overwriting initialize method:

classProductSearchincludeSearchObject.moduleoption:nameoption:category_namedefinitialize(user,options={})superoptions.merge(scope: Product.visible_to(user))endend

Or you can add simple pagination by overwriting both initialize and fetch_results (used for fetching results):

classProductSearchincludeSearchObject.modulescope{Product.all}option:nameoption:category_nameattr_reader:pagedefinitialize(filters={},page=0)superfilters@page=page.to_i.absenddeffetch_resultssuper.paginatepage: @pageendend

Extracting Basic Module

You can extarct a basic search class for your application.

classBaseSearchincludeSearchObject.module# ... options and configurationend

Then use it like:

classProductSearch < BaseSearchscope{Product}end

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Run the tests (rake)
  6. Create new Pull Request

Authors

See also the list of contributors who participated in this project.

License

MIT License

About

Search object DSL

Topics

Resources

Stars

181 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages