# Rokaki [![Gem Version](https://badge.fury.io/rb/rokaki.svg)](https://badge.fury.io/rb/rokaki) This gem was born out of a desire to dry up filtering services in Rails apps or any Ruby app that uses the concept of "filters" or "facets". There are two modes of use `Filterable` and `FilterModel` that can be activated through the use of two mixins respectively, `include Rokaki::Filterable` or `include Rokaki::FilterModel`. ## Installation Add this line to your application's Gemfile: You can install from Rubygems: ``` gem 'rokaki' ``` Or from github ```ruby gem 'rokaki', git: 'https://github.com/tevio/rokaki.git' ``` And then execute: $ bundle ## `Rokaki::Filterable` - Usage To use the DSL first include the `Rokaki::Filterable` module in your [por](http://blog.jayfields.com/2007/10/ruby-poro.html) class. ### `#define_filter_keys` #### A Simple Example A simple example might be:- ```ruby class FilterArticles include Rokaki::Filterable def initialize(filters:) @filters = filters @articles = Article end attr_accessor :filters define_filter_keys :date, author: [:first_name, :last_name] def filter_results @articles = @articles.where(date: date) if date @articles = @articles.joins(:author).where(author: { first_name: author_first_name }) if author_first_name end end article_filter = FilterArticles.new(filters: { date: '10-10-10', author: { first_name: 'Steve', last_name: 'Martin' }}) article_filter.author_first_name == 'Steve' article_filter.author_last_name == 'Martin' article_filter.date == '10-10-10' ``` In this example Rokaki maps the "flat" attribute "keys" `date`, `author_first_name` and `author_last_name` to a `@filters` object with the expected deep structure `{ date: '10-10-10', author: { first_name: 'Steve' } }`, to make it simple to use them in filter queries. #### A More Complex Example ```ruby class AdvancedFilterable include Rokaki::Filterable def initialize(filters:) @fyltrz = filters end attr_accessor :fyltrz filterable_object_name :fyltrz filter_key_infix :__ define_filter_keys :basic, advanced: { filter_key_1: [:filter_key_2, { filter_key_3: :deep_node }], filter_key_4: :deep_leaf_array } end advanced_filterable = AdvancedFilterable.new(filters: { basic: 'ABC', advanced: { filter_key_1: { filter_key_2: '123', filter_key_3: { deep_node: 'NODE' } }, filter_key_4: { deep_leaf_array: [1,2,3,4] } } }) advanced_filterable.advanced__filter_key_4__deep_leaf_array == [1,2,3,4] advanced_filterable.advanced__filter_key_1__filter_key_3__deep_node == 'NODE' ``` ### `#define_filter_map` This method takes a single field in the passed in filters hash and maps it to fields named in the second param, this is useful if you want to search for a single value across many different fields or associated tables simultaneously. #### A Simple Example ```ruby class FilterMap include Rokaki::Filterable def initialize(fylterz:) @fylterz = fylterz end attr_accessor :fylterz filterable_object_name :fylterz define_filter_map :query, :mapped_a, association: :field end filter_map = FilterMap.new(fytlerz: { query: 'H2O' }) filter_map.mapped_a == 'H2O' filter_map.association_field = 'H2O' ``` #### Additional `Filterable` options You can specify several configuration options, for example a `filter_key_prefix` and a `filter_key_infix` to change the structure of the generated filter accessors. `filter_key_prefix :__` would result in key accessors like `__author_first_name` `filter_key_infix :__` would result in key accessors like `author__first_name` `filterable_object_name :fylterz` would use an internal filter state object named `@fyltrz` instead of the default `@filters` ## `Rokaki::FilterModel` - Usage ### ActiveRecord Include `Rokaki::FilterModel` in any ActiveRecord model (only AR >= 6.0.0 tested so far) you can generate the filter keys and the actual filter lookup code using the `filters` keyword on a model like so:- ```ruby # Given the models class Author < ActiveRecord::Base has_many :articles, inverse_of: :author end class Article < ActiveRecord::Base belongs_to :author, inverse_of: :articles, required: true end class ArticleFilter include Rokaki::FilterModel filters :date, :title, author: [:first_name, :last_name] attr_accessor :filters def initialize(filters:, model: Article) @filters = filters @model = model end end filter = ArticleFilter.new(filters: params[:filters]) filtered_results = filter.results ``` ### Arrays of params You can also filter collections of fields, simply pass an array of filter values instead of a single value, eg:- `{ date: '10-10-10', author: { first_name: ['Author1', 'Author2'] } }`. ### Partial matching You can use `like` (or, if you use postgres, the case insensitive `ilike`) to perform a partial match on a specific field, there are 3 options:- `:prefix`, `:circumfix` and `:suffix`. There are two syntaxes you can use for this:- #### 1. The `filter` command syntax ```ruby class ArticleFilter include Rokaki::FilterModel filter :article, like: { # you can use ilike here instead if you use postgres and want case insensitive results author: { first_name: :circumfix, last_name: :circumfix } }, attr_accessor :filters def initialize(filters:) @filters = filters end end ``` Or #### 2. The `filter_map` command syntax `filter_map` takes the model name, then a single 'query' field and maps it to fields named in the options, this is useful if you want to search for a single value across many different fields or associated tables simultaneously. (builds on `define_filter_map`) ```ruby class AuthorFilter include Rokaki::FilterModel filter_map :author, :query, like: { articles: { title: :circumfix, reviews: { title: :circumfix } }, } attr_accessor :filters, :model def initialize(filters:) @filters = filters end end filters = { query: "Jiddu" } filtered_authors = AuthorFilter.new(filters: filters).results ``` In the above example we search for authors who have written articles containing the word "Jiddu" in the title that also have reviews containing the sames word in their titles. The above example performs an "ALL" like query, where all fields must satisfy the query term. Conversly you can use `or` to perform an "ANY", where any of the fields within the `or` will satisfy the query term, like so:- ```ruby class AuthorFilter include Rokaki::FilterModel filter_map :author, :query, like: { articles: { title: :circumfix, or: { # the or is aware of the join and will generate a compound join aware or query reviews: { title: :circumfix } } }, } attr_accessor :filters, :model def initialize(filters:) @filters = filters end end filters = { query: "Lao" } filtered_authors = AuthorFilter.new(filters: filters).results ``` #### 3. The porcelain command syntax In this syntax you will need to provide three keywords:- `filters`, `like` and `filter_model` if you are not passing in the model type and assigning it to `@model` ```ruby class ArticleFilter include Rokaki::FilterModel filters :date, :title, author: [:first_name, :last_name] like title: :circumfix # ilike title: :circumfix # case insensitive postgres mode attr_accessor :filters def initialize(filters:, model: Article) @filters = filters @model = model end end ``` Or without the model in the initializer ```ruby class ArticleFilter include Rokaki::FilterModel filters :date, :title, author: [:first_name, :last_name] like title: :circumfix filter_model :article attr_accessor :filters def initialize(filters:) @filters = filters end end ``` Would produce a query with a LIKE which circumfixes '%' around the filter term, like:- ```ruby @model = @model.where('title LIKE :query', query: "%#{title}%") ``` ### Deep nesting You can filter joins both with basic matching and partial matching ```ruby class ArticleFilter include Rokaki::FilterModel filter :author, like: { articles: { reviews: { title: :circumfix } }, } attr_accessor :filters def initialize(filters:) @filters = filters end end ``` ### Array params You can pass array params (and partially match them), to filters (search multiple matches) in databases that support it (postgres) by passing the `db` param to the filter keyword, and passing an array of search terms at runtine ```ruby class ArticleFilter include Rokaki::FilterModel filter :article, like: { author: { first_name: :circumfix, last_name: :circumfix } }, match: %i[title created_at], db: :postgres attr_accessor :filters def initialize(filters:) @filters = filters end end filterable = ArticleFilter.new(filters: { author: { first_name: ['Match One', 'Match Two'] } } ) filterable.results ``` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/tevio/rokaki. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## Code of Conduct Everyone interacting in the Rokaki project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/tevio/rokaki/blob/master/CODE_OF_CONDUCT.md).