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 😀
Add this line to your application's Gemfile:
gem'search_object'And then execute:
$ bundle
Or install it yourself as:
$ gem install search_object
Changes are available in CHANGELOG.md
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}endThen 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' optionYou can find example of most important features and plugins - here.
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.
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 resultsOf course if you want more sophisticated pagination plugins you can use:
includeSearchObject.module(:will_paginate)includeSearchObject.module(:kaminari)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)scopeendendExtends 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 %>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}endFixing 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')Very often you will just need results of search:
ProductSearch.new(params).results == ProductSearch.results(params)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 productsclassProductSearchincludeSearchObject.modulescope{Product.all}# nil values returned from option blocks are ignoredoption(:sold){ |scope,value| scope.soldifvalue}endclassProductSearchincludeSearchObject.modulescope{Product.all}option:name# automaticly applies => { |scope, value| scope.where name: value unless value.blank? }endclassProductSearchincludeSearchObject.modulescope{Product.all}option(:date){ |scope,value| scope.by_dateparse_dates(value)}privatedefparse_dates(date_string)# some "magic" method to parse datesendendclassProductSearchincludeSearchObject.modulescope{Product.all}option:date,with: :parse_datesprivatedefparse_dates(scope,value)# some "magic" method to parse datesendendclassProductSearchincludeSearchObject.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}}endYou can have fine grained scope, by overwriting initialize method:
classProductSearchincludeSearchObject.moduleoption:nameoption:category_namedefinitialize(user,options={})superoptions.merge(scope: Product.visible_to(user))endendOr 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: @pageendendYou can extarct a basic search class for your application.
classBaseSearchincludeSearchObject.module# ... options and configurationendThen use it like:
classProductSearch < BaseSearchscope{Product}end- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Run the tests (
rake) - Create new Pull Request
- Radoslav Stankov - creator - RStankov
See also the list of contributors who participated in this project.