{"id":16486788,"url":"https://github.com/rstankov/searchobject","last_synced_at":"2025-05-16T18:05:33.690Z","repository":{"id":11169115,"uuid":"13543532","full_name":"RStankov/SearchObject","owner":"RStankov","description":"Search object DSL","archived":false,"fork":false,"pushed_at":"2024-04-20T13:41:59.000Z","size":573,"stargazers_count":182,"open_issues_count":0,"forks_count":17,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-03T16:14:28.321Z","etag":null,"topics":["filter","gem","productsearch","ruby","search","searchobject","sort-direction"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RStankov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-10-13T17:31:36.000Z","updated_at":"2024-12-31T03:56:51.000Z","dependencies_parsed_at":"2023-11-23T08:23:54.489Z","dependency_job_id":"31b21ff4-7346-472d-ad89-1f885933e763","html_url":"https://github.com/RStankov/SearchObject","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RStankov%2FSearchObject","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RStankov%2FSearchObject/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RStankov%2FSearchObject/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RStankov%2FSearchObject/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RStankov","download_url":"https://codeload.github.com/RStankov/SearchObject/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248597747,"owners_count":21130937,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["filter","gem","productsearch","ruby","search","searchobject","sort-direction"],"created_at":"2024-10-11T13:30:26.902Z","updated_at":"2025-04-12T16:38:20.400Z","avatar_url":"https://github.com/RStankov.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/search_object.svg)](http://badge.fury.io/rb/search_object)\n[![Code Climate](https://codeclimate.com/github/RStankov/SearchObject.svg)](https://codeclimate.com/github/RStankov/SearchObject)\n[![Code coverage](https://coveralls.io/repos/RStankov/SearchObject/badge.svg?branch=master)](https://coveralls.io/r/RStankov/SearchObject)\n\n# SearchObject\n\nDSL for building search objects.\n\nSearch objects start with an initial collection (scope) and allow it to be filtered based on various options.\n\nUses:\n\n- complicated search forms ([example](./example/app/models/post_search.rb))\n- API endpoints with multiple filter conditions\n- [GraphQL](https://rmosolgo.github.io/graphql-ruby/) resolvers ([example](#graphql-plugin))\n- ... search objects 😀\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'search_object'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install search_object\n\n\n## Changelog\n\nChanges are available in [CHANGELOG.md](./CHANGELOG.md)\n\n## Usage\n\nJust include the ```SearchObject.module``` and define your search options:\n\n```ruby\nclass PostSearch\n  include SearchObject.module\n\n  scope { Post.all }\n\n  option(:name)             { |scope, value| scope.where name: value }\n  option(:created_at)       { |scope, dates| scope.created_after dates }\n  option(:published, false) { |scope, value| value ? scope.unopened : scope.opened }\nend\n```\n\nThen you can just search the given scope:\n\n```ruby\nsearch = PostSearch.new(filters: params[:filters])\n\n# accessing search options\nsearch.name                        # =\u003e name option\nsearch.created_at                  # =\u003e created at option\n\n# accessing results\nsearch.count                       # =\u003e number of found results\nsearch.results?                    # =\u003e is there any results found\nsearch.results                     # =\u003e found results\n\n# params for url generations\nsearch.params                      # =\u003e option values\nsearch.params opened: false        # =\u003e overwrites the 'opened' option\n```\n\n### Example\n\nYou can find example of most important features and plugins - [here](./example).\n\n## Plugins\n\n```SearchObject``` support plugins, which are passed to ```SearchObject.module``` method.\n\nPlugins are just plain Ruby modules, which are included with ```SearchObject.module```. They are located under ```SearchObject::Plugin``` module.\n\n### Paginate Plugin\n\nReally simple paginate plugin, which uses the plain ```.limit``` and ```.offset``` methods.\n\n```ruby\nclass ProductSearch\n  include SearchObject.module(:paging)\n\n  scope { Product.all }\n\n  option :name\n  option :category_name\n\n  # per page defaults to 10\n  per_page 10\n\n  # range of values is also possible\n  min_per_page 5\n  max_per_page 100\nend\n\nsearch = ProductSearch.new(filters: params[:filters], page: params[:page], per_page: params[:per_page])\n\nsearch.page                                                 # =\u003e page number\nsearch.per_page                                             # =\u003e per page (10)\nsearch.results                                              # =\u003e paginated page results\n```\n\nOf course if you want more sophisticated pagination plugins you can use:\n\n```ruby\ninclude SearchObject.module(:will_paginate)\ninclude SearchObject.module(:kaminari)\n```\n\n### Enum Plugin\n\nGives you filter with pre-defined options.\n\n```ruby\nclass ProductSearch\n  include SearchObject.module(:enum)\n\n  scope { Product.all }\n\n  option :order, enum: %w(popular date)\n\n  private\n\n  # Gets called when order with 'popular' is given\n  def apply_order_with_popular(scope)\n    scope.by_popularity\n  end\n\n  # Gets called when order with 'date' is given\n  def apply_order_with_date(scope)\n    scope.by_date\n  end\n\n  # (optional) Gets called when invalid enum is given\n  def handle_invalid_order(scope, invalid_value)\n    scope\n  end\nend\n```\n\n### Model Plugin\n\nExtends your search object with ```ActiveModel```, so you can use it in Rails forms.\n\n```ruby\nclass ProductSearch\n  include SearchObject.module(:model)\n\n  scope { Product.all }\n\n  option :name\n  option :category_name\nend\n```\n\n```erb\n\u003c%# in some view: %\u003e\n\n\u003c%= form_for ProductSearch.new do |form| %\u003e\n  \u003c% form.label :name %\u003e\n  \u003c% form.text_field :name %\u003e\n  \u003c% form.label :category_name %\u003e\n  \u003c% form.text_field :category_name %\u003e\n\u003c% end %\u003e\n```\n\n### GraphQL Plugin\n\nInstalled as separate [gem](https://github.com/RStankov/SearchObjectGraphQL), it is designed to work with GraphQL:\n\n```\ngem 'search_object_graphql'\n```\n\n```ruby\nclass PostResolver\n  include SearchObject.module(:graphql)\n\n  type PostType\n\n  scope { Post.all }\n\n  option(:name, type: types.String)       { |scope, value| scope.where name: value }\n  option(:published, type: types.Boolean) { |scope, value| value ? scope.published : scope.unpublished }\nend\n```\n\n### Sorting Plugin\n\nFixing the pain of dealing with sorting attributes and directions.\n\n```ruby\nclass ProductSearch\n  include SearchObject.module(:sorting)\n\n  scope { Product.all }\n\n  sort_by :name, :price\nend\n\nsearch = ProductSearch.new(filters: {sort: 'price desc'})\n\nsearch.results                                # =\u003e Product sorted my price DESC\nsearch.sort_attribute                         # =\u003e 'price'\nsearch.sort_direction                         # =\u003e 'desc'\n\n# Smart sort checking\nsearch.sort?('price')                         # =\u003e true\nsearch.sort?('price desc')                    # =\u003e true\nsearch.sort?('price asc')                     # =\u003e false\n\n# Helpers for dealing with reversing sort direction\nsearch.reverted_sort_direction                # =\u003e 'asc'\nsearch.sort_direction_for('price')            # =\u003e 'asc'\nsearch.sort_direction_for('name')             # =\u003e 'desc'\n\n# Params for sorting links\nsearch.sort_params_for('name')\n\n```\n\n## Tips \u0026 Tricks\n\n### Results Shortcut\n\nVery often you will just need results of search:\n\n```ruby\nProductSearch.new(params).results == ProductSearch.results(params)\n```\n\n### Passing Scope as Argument\n\n``` ruby\nclass ProductSearch\n  include SearchObject.module\nend\n\n# first arguments is treated as scope (if no scope option is provided)\nsearch = ProductSearch.new(scope: Product.visible, filters: params[:f])\nsearch.results # =\u003e includes only visible products\n```\n\n### Handling Nil Options\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { Product.all }\n\n  # nil values returned from option blocks are ignored\n  option(:sold) { |scope, value| scope.sold if value }\nend\n```\n\n### Default Option Block\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { Product.all }\n\n  option :name # automaticly applies =\u003e { |scope, value| scope.where name: value unless value.blank? }\nend\n```\n\n### Using Instance Method in Option Blocks\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { Product.all }\n\n  option(:date) { |scope, value| scope.by_date parse_dates(value) }\n\n  private\n\n  def parse_dates(date_string)\n    # some \"magic\" method to parse dates\n  end\nend\n```\n\n### Using Instance Method for Straight Dispatch\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { Product.all }\n\n  option :date, with: :parse_dates\n\n  private\n\n  def parse_dates(scope, value)\n    # some \"magic\" method to parse dates\n  end\nend\n```\n\n### Active Record Is Not Required\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { RemoteEndpoint.fetch_product_as_hashes }\n\n  option(:name)     { |scope, value| scope.select { |product| product[:name] == value } }\n  option(:category) { |scope, value| scope.select { |product| product[:category] == value } }\nend\n```\n\n### Overwriting Methods\n\nYou can have fine grained scope, by overwriting ```initialize``` method:\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  option :name\n  option :category_name\n\n  def initialize(user, options = {})\n    super options.merge(scope: Product.visible_to(user))\n  end\nend\n```\n\nOr you can add simple pagination by overwriting both ```initialize``` and ```fetch_results``` (used for fetching results):\n\n```ruby\nclass ProductSearch\n  include SearchObject.module\n\n  scope { Product.all }\n\n  option :name\n  option :category_name\n\n  attr_reader :page\n\n  def initialize(filters = {}, page = 0)\n    super filters\n    @page = page.to_i.abs\n  end\n\n  def fetch_results\n    super.paginate page: @page\n  end\nend\n```\n\n### Extracting Basic Module\n\nYou can extarct a basic search class for your application.\n\n```ruby\nclass BaseSearch\n  include SearchObject.module\n\n  # ... options and configuration\nend\n```\n\n Then use it like:\n\n ```ruby\nclass ProductSearch \u003c BaseSearch\n  scope { Product }\nend\n```\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Run the tests (`rake`)\n6. Create new Pull Request\n\n## Authors\n\n* **Radoslav Stankov** - *creator* - [RStankov](https://github.com/RStankov)\n\nSee also the list of [contributors](./contributors) who participated in this project.\n\n## License\n\n**[MIT License](./LICENSE.txt)**\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frstankov%2Fsearchobject","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frstankov%2Fsearchobject","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frstankov%2Fsearchobject/lists"}