{"id":15288762,"url":"https://github.com/rocksolt/filterameter","last_synced_at":"2026-04-02T19:18:00.597Z","repository":{"id":41824087,"uuid":"224731054","full_name":"RockSolt/filterameter","owner":"RockSolt","description":"Simplify and speed development of Rails controllers by making filter parameters declarative.","archived":false,"fork":false,"pushed_at":"2025-03-15T23:53:00.000Z","size":286,"stargazers_count":90,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-04T09:44:49.740Z","etag":null,"topics":["filters","rails","ruby","search"],"latest_commit_sha":null,"homepage":"https://rockridgesolutions.com/posts/filterameter","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/RockSolt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2019-11-28T21:15:49.000Z","updated_at":"2025-03-18T17:35:18.000Z","dependencies_parsed_at":"2024-01-12T21:58:23.825Z","dependency_job_id":"96c59200-98e9-4aa7-b403-18700da037cc","html_url":"https://github.com/RockSolt/filterameter","commit_stats":{"total_commits":165,"total_committers":3,"mean_commits":55.0,"dds":"0.16969696969696968","last_synced_commit":"6743706926170ac9f1ff614696652a7c52e3ec0c"},"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RockSolt%2Ffilterameter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RockSolt%2Ffilterameter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RockSolt%2Ffilterameter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RockSolt%2Ffilterameter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RockSolt","download_url":"https://codeload.github.com/RockSolt/filterameter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247157281,"owners_count":20893220,"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":["filters","rails","ruby","search"],"created_at":"2024-09-30T15:53:06.811Z","updated_at":"2026-04-02T19:18:00.577Z","avatar_url":"https://github.com/RockSolt.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/filterameter.svg)](https://badge.fury.io/rb/filterameter)\n[![RuboCop](https://github.com/RockSolt/filterameter/workflows/RuboCop/badge.svg)](https://github.com/RockSolt/filterameter/actions?query=workflow%3ARuboCop)\n[![RSpec](https://github.com/RockSolt/filterameter/workflows/RSpec/badge.svg)](https://github.com/RockSolt/filterameter/actions?query=workflow%3ARSpec)\n\n# Filterameter\nFilterameter provides declarative filters for Rails controllers to reduce boilerplate code and increase readability. How many times have you seen (or written) this controller action?\n\n```ruby\ndef index\n  @films = Films.all\n  @films = @films.where(name: params[:name]) if params[:name]\n  @films = @films.joins(:film_locations).merge(FilmLocations.where(location_id: params[:location_id])) if params[:location_id]\n  @films = @films.directed_by(params[:director_id]) if params[:director_id]\n  @films = @films.written_by(params[:writer_id]) if params[:writer_id]\n  @films = @films.acted_by(params[:actor_id]) if params[:actor_id]\nend\n```\n\nIt's redundant code and a bit of a pain to write and maintain. Not to mention what RuboCop is going to say about it. Wouldn't it be nice if you could just declare the filters that the controller accepts?\n\n```ruby\n  filter :name, partial: true\n  filter :location_id, association: :film_locations\n  filter :director_id, name: :directed_by\n  filter :writer_id, name: :written_by\n  filter :actor_id, name: :acted_by\n\n  def index\n    @films = build_query_from_filters\n  end\n```\n\nSimplify and speed development of Rails controllers by making filter parameters declarative with Filterameter.\n\n## Table of Contents\n- [Getting Started](#getting-started)\n- [Usage](#usage)\n  - [Filtering Options](#filtering-options)\n    - [Name](#name)\n    - [Association](#association)\n    - [Validates](#validates)\n    - [Partial](#partial)\n    - [Range](#range)\n    - [Sortable](#sortable)\n  - [Scope Filters](#scope-filters)\n  - [Sorting](#sorting)\n  - [Building the Query](#building-the-query)\n  - [Specifying the Model](#specifying-the-model)\n- [Configuration](#configuration)\n- [Testing Declarations](#testing-declarations)\n- [Forms and Query Parameters](#forms-and-query-parameters)\n- [Contribute](#contribute)\n- [License](#license)\n\n## Getting Started\n\nThis gem requires Rails 6.1+, and works with ActiveRecord.\n\n### Installation\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'filterameter'\n```\n\nAnd then execute:\n```bash\n$ bundle install\n```\n\nOr install it yourself as:\n```bash\n$ gem install filterameter\n```\n\n## Usage\nInclude module `Filterameter::DeclarativeFilters` in the controller to provide the filter DSL. It can be included in the `ApplicationController` to make the functionality available to all controllers or it can be mixed in on a case-by-case basis.\n\n```ruby\n  filter :color\n  filter :size, validates: { inclusion: { in: %w[Small Medium Large], allow_multiple_values: true } }\n  filter :brand_name, association: :brand, name: :name\n  filter :on_sale, association: :price, validates: [{ numericality: { greater_than: 0 } },\n                                                    { numericality: { less_than: 100 } }]\n```\n\nFilters without options can be declared all at once with `filters`:\n\n```ruby\nfilters :color,\n        :size,\n        :name\n```\n\n### Filtering Options\n\nThe following options can be specified for each filter.\n\n#### name\nIf the name of the parameter is different than the name of the attribute or scope, then use the name parameter to specify the name of the attribute or scope. For example, if the attribute name is `current_status` but the filter is exposed simply as `status` use the following:\n\n```ruby\nfilter :status, name: :current_status\n```\n\nThis option can also be helpful with nested filters so that the query parameter can be prefixed with the model name. See the `association` option for an example.\n\n#### association\nIf the attribute or scope is nested, it can be referenced by naming the association. For example, if the manager_id attribute lives on an employee's department record, use the following:\n\n```ruby\nfilter :manager_id, association: :department\n```\n\nThe attribute or scope can be nested more than one level. Declare the filter with an array specifying the associations in order. For example, if an employee belongs to a department and a department belongs to a business unit, use the following to query on the business unit name:\n\n```ruby\nfilter :business_unit_name, name: :name, association: [:department, :business_unit]\n```\n\nIf an association is a `has_many` [the distinct method](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-distinct) is called on the query.\n\n_Limitation:_ If there is more than one association to the same table _and_ both associations can be part of the query, then you cannot use a nested filter directly. Instead, build a scope that disambiguates the associations then build a filter against that scope.\n\n#### validates\nIf the filter value should be validated, use the `validates` option along with [ActiveModel validations](https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validates). Here's an example of the inclusion validator being used to restrict sizes:\n\n```ruby\nfilter :size, validates: { inclusion: { in: %w[Small Medium Large] } }\n```\n\nThe `inclusion` validator has been overridden to provide the additional option `allow_multiple_values`. When true, the value can be an array and each entry in the array will be validated. Use this when the filter can specify one or more values.\n\n```ruby\nfilter :size, validates: { inclusion: { in: %w[Small Medium Large], allow_multiple_values: true } }\n```\n\n\n#### partial\nSpecify the partial option if the filter should do a partial search (SQL's `LIKE`). The partial option accepts a hash to specify the search behavior. Here are the available options:\n- match: anywhere (default), from_start, dynamic\n- case_sensitive: true, false (default)\n\nThere are two shortcuts: : the partial option can be declared with `true`, which just uses the defaults; or the partial option can be declared with the match option directly, such as `partial: :from_start`.\n\n```ruby\nfilter :description, partial: true\nfilter :department_name, partial: :from_start\nfilter :reason, partial: { match: :dynamic, case_sensitive: true }\n```\n\nThe `match` options defines where you are searching (which then controls where the wildcard(s) appear):\n- anywhere: adds wildcards at the start and end, for example '%blue%'\n- from_start: adds a wildcard at the end, for example 'blue%'\n- dynamic: adds no wildcards; this enables the client to fully control the search string\n\n#### range\nSpecify the range option to enable searches by ranges, minimum values, or maximum values. (All of these are inclusive. A search for a minimum value of $10.00 would include all items priced at $10.00.)\n\nHere are the available options:\n- true: enable ranges, minimum values, and/or maximum values\n- min_only: enables minimum values\n- max_only: enables maximum values\n\nUsing the range option means that _in addition to the attribute filter_ minimum and maximum query parameters may also be specified. The parameter names are the attribute name plus the suffix \u003ctt\u003e_min\u003c/tt\u003e or \u003ctt\u003e_max\u003c/tt\u003e.\n\n```ruby\nfilter :price, range: true\nfilter :approved_at, range: :min_only\nfilter :sale_price, range: :max_only\n```\n\nIn the first example, query parameters could include \u003ctt\u003eprice\u003c/tt\u003e, \u003ctt\u003eprice_min\u003c/tt\u003e, and \u003ctt\u003eprice_max\u003c/tt\u003e.\n\n#### sortable\n\nBy default most filters are sortable. To prevent an attribute filter from being sortable, set the option to false.\n\n```ruby\nfilter :price, sortable: false\n```\n\nThe following filters are not sortable:\n\n- scope filters (see [_Sorting with a Scope_](#sorting-with-a-scope))\n- filters with collection associations\n\n\n### Scope Filters\n\nFor scopes that do not take arguments, the filter should provide a boolean that indicates whether or not the scope should be invoked. For example, imagine a scope called `high_priority` with criteria that identifies high priority records. The scope would be invoked by the query parameters `high_priority=true`.\n\nPassing `high_priority=false` will not invoke the scope. This makes it easy to include a filter with a check box UI.\n\nScopes that do take arguments [must be written as class methods, not inline scopes.](https://guides.rubyonrails.org/active_record_querying.html#passing-in-arguments) For example, imagine a scope called `recent` that takes an as of date as an argument. Here is what that might look like:\n\n```ruby\ndef self.recent(as_of_date)\n  where('created_at \u003e ?', as_of_date)\nend\n```\n\n### Sorting\n\nAs noted above, most attribute filters are sortable by default. If no filter has been declared for an attribute, the `sort` declaration can be used. Use the same `name` and `association` options as needed.\n\nFor example, the following declaration could be used on an activity controller to allow activities to be sorted by project created at.\n\n```ruby\nsort :project_created_at, name: :created_at, association: :project\n```\n\nSorts without options can be declared all at once with `sorts`:\n\n```ruby\nsorts :created_at,\n      :updated_at,\n      :description\n```\n\n#### Sorting with a Scope\n\nScopes can be used for sorting, but must be declared with `sort` (or `sorts`). For example, if a model included a scope called `by_created_at` you could add the following to the controller to expose it.\n\n```ruby\nsort :by_created_at\n```\n\nThe `name` and `association` options can also be used. For example, if the scope was on the Project model it could also be used on a child Activity controller using the `association` option:\n\n```ruby\nsort :by_created_at, association: :project\n```\n\nOnly singular associations are valid for sorting. A collection association could return multiple values, making the sort indeterminate.\n\nA scope that is used for sorting must accept a single argument. It will be passed either `:asc` or `:desc` depending on the parameter.\n\nThe example scope above might be defined as follows:\n\n```ruby\ndef self.by_created_at(dir)\n  order(created_at: dir)\nend\n```\n\n#### Default Sort\n\nA default sort can be declared using `default_sort`. The argument(s) should specify one or more of the declared sorts or sortable filters by name. The sorts should be defined as key-value pairs, with the name as the key and the direction as the value.\n\n```ruby\ndefault_sort updated_at: :desc, description: :asc\n```\n\nIn order to provide consistent results, a sort is always applied. If no default is specified, it will use primary key descending.\n\n### Building the Query\n\nThere are two ways to apply the filters and build the query, depending on how much control and/or visibility is desired:\n\n- Use the `build_filtered_query` before action callback\n- Manually call `build_query_from_filters`\n\n\n#### Use the `build_filtered_query` before action callback\n\nAdd before action callback `build_filtered_query` for controller actions that should build the query. This can be done either in the `ApplicationController` or on a case-by-case basis.\n\nWhen using the callback, the variable name is the pluralized model name. For example, the Photo model will use the variable `@photos` to store the query. The variable name can be explicitly specified with `filter_query_var_name`. For example, if the query is stored as `@data`, use the following:\n\n```ruby\nfilter_query_var_name :data\n```\n\nAdditionally, the `filter_model` command takes an optional second parameter to specify the variable name. Both the model and the variable name can be specified with this short-cut. For example, to use the Picture model and store the results as `@data`, use the following:\n\n```ruby\nfilter_model 'Picture', :data\n```\n\n##### Example\n\nIn the happy path, the WidgetsController serves Widgets and can filter on size and color. Here's what the controller might look like:\n\n```ruby\nclass WidgetsController \u003c ApplicationController\n  include Filterameter::DeclarativeFilters\n  before_action :build_filtered_query, only: :index\n\n  filter :size\n  filter :color\n\n  def index\n    render json: @widgets\n  end\nend\n```\n\n#### Manually call `build_query_from_filters`\n\nTo generate the query manually, you can call `build_query_from_filters` directly _instead of using the callback_.\n\n###### Example\n\nHere's the Widgets controller again, this time building the query manually:\n\n```ruby\nclass WidgetsController \u003c ApplicationController\n  include Filterameter::DeclarativeFilters\n\n  filter :size\n  filter :color\n\n  def index\n    @widgets = build_query_from_filters\n  end\nend\n```\n\nThis method optionally takes a starting query. If there was a controller for Active Widgets that should only return active widgets, the following could be passed into the method as the starting point:\n\n```ruby\n  def index\n    @widgets = build_query_from_filters(Widget.where(active: true))\n  end\n```\n\nThe starting query is also a good place to provide any includes to enable eager loading:\n\n```ruby\n  def index\n    @widgets = build_query_from_filters(Widgets.includes(:manufacturer))\n  end\n```\n\nNote that the starting query provides the model, so the model is not looked up and the `model_name` declaration in not needed.\n\n### Specifying the Model\n\nRails conventions are used to determine the controller's model. For example, the PhotosController builds a query against the Photo model. If a controller is namespaced, the model will first be looked up without the namespace, then with the namespace.\n\n**If the conventions do not provide the correct model**, the model can be named explicitly with the following:\n\n```ruby\nfilter_model 'Picture'\n```\n\n_Important:_ If the `filter_model` declaration is used, it must be before any filter or sort declarations.\n\n## Configuration\n\nThere are three configuration options:\n\n- action_on_undeclared_parameters\n- action_on_validation_failure\n- filter_key\n\nThe configuration options can be set in an initializer, an environment file, or in `application.rb`.\n\nThe options can be set directly...\n\n`Filterameter.configuration.action_on_undeclared_parameters = :log`\n\n...or the configuration can be yielded:\n\n```ruby\nFilterameter.configure do |config|\n  config.action_on_undeclared_parameters = :log\n  config.action_on_validation_failure = :log\n  config.filter_key = :f\nend\n```\n\n#### Action On Undeclared Parameters\n\nOccurs when the filter parameter contains any keys that are not defined. Valid actions are `:log`, `:raise`, and `false` (do not take action). By default, development will log, test will raise, and production will do nothing.\n\n#### Action on Validation Failure\n\nOccurs when a filter parameter fails a validation. Valid actions are `:log`, `:raise`, and `false` (do not take action). By default, development will log, test will raise, and production will do nothing.\n\n#### Filter Key\n\nBy default, the filter parameters are nested under the key `:filter`. Use this setting to override the key.\n\nIf the filter parameters are NOT nested, set this to false. Doing so will restrict the filter parameters to only\nthose that have been declared, meaning undeclared parameters are ignored (and the action_on_undeclared_parameters\nconfiguration option does not come into play).\n\n## Testing Declarations\n\nThe declarations can be tested for each controller, catching typos, incorrectly defined scopes, or any other issues. Method `declarations_validator` is added to each controller, and a single controller test can be added to validate all the declarations for that controller.\n\nAn RSpec test might look like this:\n\n```ruby\nexpect(WidgetsController.declarations_validator).to be_valid\n```\n\nIn Minitest it might look like this:\n\n```ruby\nvalidator = WidgetsController.declarations_validator\nassert_predicate validator, :valid?, -\u003e { validator.errors }\n```\n\n## Forms and Query Parameters\n\nThe filter parameters are pulled from the controller parameters, nested under the key `filter` (by default; see [Configuration](#configuration) to change the filter key). For example a request for large, blue widgets might have the following query parameters on the url:\n\n```\n?filter[size]=large\u0026filter[color]=blue\n```\n\nOn [a generic search form](https://guides.rubyonrails.org/form_helpers.html#a-generic-search-form), the [`form_with` form helper takes the option `scope`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) that allows parameters to be grouped:\n\n```erb\n\u003c%= form_with url: \"/search\", scope: :filter, method: :get do |form| %\u003e\n  \u003c%= form.label :size, \"Size:\" %\u003e\n  \u003c%= form.text_field :size %\u003e\n  \u003c%= form.label :color, \"Color:\" %\u003e\n  \u003c%= form.text_field :color %\u003e\n  \u003c%= form.submit \"Search\" %\u003e\n\u003c% end %\u003e\n```\n\n#### Sort Parameters\n\nThe sort is also nested underneath the filter key:\n\n`/widgets?filter[sort]=size`\n\nUse an array to pass multiple sorts. The order of the parameters is the order the sorts will be applied. For example, the following sorts first by size then by color:\n\n`/widgets?filter[sort][]=size\u0026filter[sort][]=color`\n\nSorts are ascending by default, but can use a prefix can be added to control the sort:\n\n- `+` ascending (the default)\n- `-` descending\n\nFor example, the following sorts by size descending:\n\n`/widgets?filter[sort]=-size`\n\n## Contribute\n\nFeedback, feature requests, and proposed changes are welcomed. Please use the [issue tracker](https://github.com/RockSolt/filterameter/issues)\nfor feedback and feature requests. To propose a change directly, please fork the repo and open a pull request. Keep an eye on the actions to make\nsure the tests and Rubocop are passing. [Code Climate](https://codeclimate.com/github/RockSolt/filterameter) is also used manually to assess the codeline.\n\nTo report a bug, please use the [issue tracker](https://github.com/RockSolt/filterameter/issues) and provide the following information:\n\n- the version in use\n- the filter declarations\n- the SQL generated (for invalid / incorrect queries)\n\nGold stars will be awarded if you are able to [replicate the issue with a test](spec/README.md).\n\n### Running Tests\n\nTests are written in RSpec.\n\n```bash\nbin/prepare_db.sh\nbundle exec rspec\n```\n\nThe tests can also be run across all the ruby and Rails combinations using appraisal. The install is also a one-time step.\n\n```bash\nbundle exec appraisal install\nbundle exec appraisal rspec\n```\n\n## License\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocksolt%2Ffilterameter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frocksolt%2Ffilterameter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocksolt%2Ffilterameter/lists"}