# 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:

```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 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).