json_api_filter 0.1.1 → 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b8ea65bebb5e2456a2009eea40099f2f000a4bc9c83ed79d3d4207c7b86cbdf
4
- data.tar.gz: eaaa75be388a45dd56755d47a94956597befeb4db09bd15de5ec8832b047fdef
3
+ metadata.gz: a0e5c959706685cf6cc047d4824c7ed0e0c35778b6b952aa3b305e5e0eb40f13
4
+ data.tar.gz: 3a87503e2341ae745d0ec6b5154049c95b0a28b527aeb8903ed000e53685bf3b
5
5
  SHA512:
6
- metadata.gz: 2c383c8c9602a81549fcc45b016b262e07d99c16c90239fcd390057503dbb3773a3ed07ccf86b06157a65b85f0d86e1742dd7465ab9c1c5bd46a0a48c46e8790
7
- data.tar.gz: 3c52afa46d6f74466f561af5a7f896a77af6b41440c8b2cac75f11c87a19f1fe26c61bdac86521853dddc8d415b257179a7d65be394083444ad2c6d82b0b4134
6
+ metadata.gz: fee62197e5d3814185e35f7935b76daba4c96dcabd2e655e250512f3267e8295512fffe16301df3b18b089c4eb5b5cbed1a120793dd60518929aa666ba16b5e2
7
+ data.tar.gz: c183c0a031bac479dfa66baf3cb90a9ef6735719c7dc61a12fcbda50412a0239e5dcd6ca4ebd3ddffae23588f9cd90793123a79cd2a44107723a9f869b5d4b2f
data/.gitignore CHANGED
@@ -9,4 +9,7 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
- /.idea/
12
+ /.idea/
13
+ /spec/dummy/db/development.sqlite3
14
+ /spec/dummy/db/test.sqlite3
15
+ /spec/dummy/log/
data/.travis.yml CHANGED
@@ -5,3 +5,6 @@ cache: bundler
5
5
  rvm:
6
6
  - 2.6.3
7
7
  before_install: gem install bundler -v 1.17.2
8
+ script:
9
+ - (cd spec/dummy && RAILS_ENV=test bundle exec rails db:setup)
10
+ - bundle exec rspec
data/Gemfile.lock CHANGED
@@ -1,39 +1,161 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_api_filter (0.1.1)
4
+ json_api_filter (0.2)
5
5
  activesupport (>= 3.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activesupport (6.1.3)
10
+ actioncable (6.1.3.1)
11
+ actionpack (= 6.1.3.1)
12
+ activesupport (= 6.1.3.1)
13
+ nio4r (~> 2.0)
14
+ websocket-driver (>= 0.6.1)
15
+ actionmailbox (6.1.3.1)
16
+ actionpack (= 6.1.3.1)
17
+ activejob (= 6.1.3.1)
18
+ activerecord (= 6.1.3.1)
19
+ activestorage (= 6.1.3.1)
20
+ activesupport (= 6.1.3.1)
21
+ mail (>= 2.7.1)
22
+ actionmailer (6.1.3.1)
23
+ actionpack (= 6.1.3.1)
24
+ actionview (= 6.1.3.1)
25
+ activejob (= 6.1.3.1)
26
+ activesupport (= 6.1.3.1)
27
+ mail (~> 2.5, >= 2.5.4)
28
+ rails-dom-testing (~> 2.0)
29
+ actionpack (6.1.3.1)
30
+ actionview (= 6.1.3.1)
31
+ activesupport (= 6.1.3.1)
32
+ rack (~> 2.0, >= 2.0.9)
33
+ rack-test (>= 0.6.3)
34
+ rails-dom-testing (~> 2.0)
35
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
36
+ actiontext (6.1.3.1)
37
+ actionpack (= 6.1.3.1)
38
+ activerecord (= 6.1.3.1)
39
+ activestorage (= 6.1.3.1)
40
+ activesupport (= 6.1.3.1)
41
+ nokogiri (>= 1.8.5)
42
+ actionview (6.1.3.1)
43
+ activesupport (= 6.1.3.1)
44
+ builder (~> 3.1)
45
+ erubi (~> 1.4)
46
+ rails-dom-testing (~> 2.0)
47
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
48
+ activejob (6.1.3.1)
49
+ activesupport (= 6.1.3.1)
50
+ globalid (>= 0.3.6)
51
+ activemodel (6.1.3.1)
52
+ activesupport (= 6.1.3.1)
53
+ activerecord (6.1.3.1)
54
+ activemodel (= 6.1.3.1)
55
+ activesupport (= 6.1.3.1)
56
+ activestorage (6.1.3.1)
57
+ actionpack (= 6.1.3.1)
58
+ activejob (= 6.1.3.1)
59
+ activerecord (= 6.1.3.1)
60
+ activesupport (= 6.1.3.1)
61
+ marcel (~> 1.0.0)
62
+ mini_mime (~> 1.0.2)
63
+ activesupport (6.1.3.1)
11
64
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
65
  i18n (>= 1.6, < 2)
13
66
  minitest (>= 5.1)
14
67
  tzinfo (~> 2.0)
15
68
  zeitwerk (~> 2.3)
69
+ builder (3.2.4)
70
+ byebug (11.1.3)
16
71
  concurrent-ruby (1.1.8)
17
- diff-lcs (1.3)
18
- i18n (1.8.9)
72
+ crass (1.0.6)
73
+ diff-lcs (1.4.4)
74
+ erubi (1.10.0)
75
+ globalid (0.4.2)
76
+ activesupport (>= 4.2.0)
77
+ i18n (1.8.10)
19
78
  concurrent-ruby (~> 1.0)
79
+ loofah (2.9.0)
80
+ crass (~> 1.0.2)
81
+ nokogiri (>= 1.5.9)
82
+ mail (2.7.1)
83
+ mini_mime (>= 0.1.1)
84
+ marcel (1.0.1)
85
+ method_source (1.0.0)
86
+ mini_mime (1.0.3)
87
+ mini_portile2 (2.5.0)
20
88
  minitest (5.14.4)
21
- rake (13.0.1)
22
- rspec (3.9.0)
23
- rspec-core (~> 3.9.0)
24
- rspec-expectations (~> 3.9.0)
25
- rspec-mocks (~> 3.9.0)
26
- rspec-core (3.9.0)
27
- rspec-support (~> 3.9.0)
28
- rspec-expectations (3.9.0)
89
+ nio4r (2.5.7)
90
+ nokogiri (1.11.2)
91
+ mini_portile2 (~> 2.5.0)
92
+ racc (~> 1.4)
93
+ racc (1.5.2)
94
+ rack (2.2.3)
95
+ rack-test (1.1.0)
96
+ rack (>= 1.0, < 3)
97
+ rails (6.1.3.1)
98
+ actioncable (= 6.1.3.1)
99
+ actionmailbox (= 6.1.3.1)
100
+ actionmailer (= 6.1.3.1)
101
+ actionpack (= 6.1.3.1)
102
+ actiontext (= 6.1.3.1)
103
+ actionview (= 6.1.3.1)
104
+ activejob (= 6.1.3.1)
105
+ activemodel (= 6.1.3.1)
106
+ activerecord (= 6.1.3.1)
107
+ activestorage (= 6.1.3.1)
108
+ activesupport (= 6.1.3.1)
109
+ bundler (>= 1.15.0)
110
+ railties (= 6.1.3.1)
111
+ sprockets-rails (>= 2.0.0)
112
+ rails-dom-testing (2.0.3)
113
+ activesupport (>= 4.2.0)
114
+ nokogiri (>= 1.6)
115
+ rails-html-sanitizer (1.3.0)
116
+ loofah (~> 2.3)
117
+ railties (6.1.3.1)
118
+ actionpack (= 6.1.3.1)
119
+ activesupport (= 6.1.3.1)
120
+ method_source
121
+ rake (>= 0.8.7)
122
+ thor (~> 1.0)
123
+ rake (13.0.3)
124
+ rspec (3.10.0)
125
+ rspec-core (~> 3.10.0)
126
+ rspec-expectations (~> 3.10.0)
127
+ rspec-mocks (~> 3.10.0)
128
+ rspec-core (3.10.1)
129
+ rspec-support (~> 3.10.0)
130
+ rspec-expectations (3.10.1)
29
131
  diff-lcs (>= 1.2.0, < 2.0)
30
- rspec-support (~> 3.9.0)
31
- rspec-mocks (3.9.0)
132
+ rspec-support (~> 3.10.0)
133
+ rspec-mocks (3.10.2)
32
134
  diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.9.0)
34
- rspec-support (3.9.0)
135
+ rspec-support (~> 3.10.0)
136
+ rspec-rails (5.0.1)
137
+ actionpack (>= 5.2)
138
+ activesupport (>= 5.2)
139
+ railties (>= 5.2)
140
+ rspec-core (~> 3.10)
141
+ rspec-expectations (~> 3.10)
142
+ rspec-mocks (~> 3.10)
143
+ rspec-support (~> 3.10)
144
+ rspec-support (3.10.2)
145
+ sprockets (4.0.2)
146
+ concurrent-ruby (~> 1.0)
147
+ rack (> 1, < 3)
148
+ sprockets-rails (3.2.2)
149
+ actionpack (>= 4.0)
150
+ activesupport (>= 4.0)
151
+ sprockets (>= 3.0.0)
152
+ sqlite3 (1.4.2)
153
+ thor (1.1.0)
35
154
  tzinfo (2.0.4)
36
155
  concurrent-ruby (~> 1.0)
156
+ websocket-driver (0.7.3)
157
+ websocket-extensions (>= 0.1.0)
158
+ websocket-extensions (0.1.5)
37
159
  zeitwerk (2.4.2)
38
160
 
39
161
  PLATFORMS
@@ -41,9 +163,13 @@ PLATFORMS
41
163
 
42
164
  DEPENDENCIES
43
165
  bundler (~> 1.16)
166
+ byebug
44
167
  json_api_filter!
168
+ rails
45
169
  rake (~> 13.0)
46
- rspec (~> 3.0)
170
+ rspec (>= 3.0)
171
+ rspec-rails
172
+ sqlite3
47
173
 
48
174
  BUNDLED WITH
49
175
  1.17.2
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # JsonApiFilter
2
2
 
3
- Filter for rails controller based on JsonAPI spec: `/books?filter[library_id]=1,2&filter[author_id]=12`
3
+ Filter for rails controller based on JsonAPI spec: `/books?filter[library_id]=1,2&filter[author_id][eq]=12&filter[created_at][gt]=2021-02-02`
4
4
 
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/f620f88131ea9d8ff650/maintainability)](https://codeclimate.com/github/Blaked84/json_api_filter/maintainability)
5
6
  [![Gem Version](https://badge.fury.io/rb/json_api_filter.svg)](https://badge.fury.io/rb/json_api_filter)
7
+ [![Build Status](https://travis-ci.com/Blaked84/json_api_filter.svg?branch=master)](https://travis-ci.com/Blaked84/json_api_filter)
6
8
 
7
9
  ## Installation
8
10
 
@@ -13,27 +15,30 @@ gem 'json_api_filter'
13
15
  ```
14
16
 
15
17
  And then execute:
16
-
17
- $ bundle
18
+ ```bash
19
+ $ bundle
20
+ ```
18
21
 
19
22
  Or install it yourself as:
20
-
21
- $ gem install json_api_filter
23
+ ```bash
24
+ $ gem install json_api_filter
25
+ ```
22
26
 
23
27
  ## Usage
24
28
 
25
29
  ### Quick start
26
30
 
27
- To filter this request `/books?filter[library_id]=1,2&filter[author_id]=12`
31
+ To filter this request `/books?filter[library_id]=1,2&filter[author_id]=12&search=Lord of the ring`
28
32
 
29
33
  ```ruby
30
34
  class Book < ApplicationController
31
35
 
32
36
  include JsonApiFilter
33
37
  permitted_filters %i[library_id author_id]
38
+ permitted_searches :user_search
34
39
 
35
40
  def index
36
- @books = Book.all.where(attr_filter(params))
41
+ @books = json_api_filter(Book, params)
37
42
  end
38
43
 
39
44
  end
@@ -41,7 +46,27 @@ end
41
46
  ```
42
47
 
43
48
  - `permitted_filters` let you define allowed attributes to filter on (mandatory)
44
- - `filter_by_attr(params)` return a hash with the filter from the request params
49
+ - `permitted_searches` let you define the allowed search method defined in you model what will be called if you pass `search` params in your request (can be a pg_search scope)
50
+ - `json_api_filter(scope, params)` return an active record relation (`Book::` in this example)
51
+
52
+ ## Migration from 0.1
53
+ 0.2.x version is not compatible with 0.1
54
+ In your controller, you will have to replace all occurrences of `attr_filter` as bellow :
55
+
56
+ ### Before
57
+ ```ruby
58
+ def index
59
+ @books = Book.all.where(attr_filter(params))
60
+ end
61
+ ```
62
+
63
+ ### After
64
+ ```ruby
65
+ def index
66
+ @books = json_api_filter(Book, params)
67
+ end
68
+ ```
69
+
45
70
  ## Development
46
71
 
47
72
  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.
@@ -37,6 +37,10 @@ Gem::Specification.new do |spec|
37
37
 
38
38
  spec.add_development_dependency "bundler", "~> 1.16"
39
39
  spec.add_development_dependency "rake", "~> 13.0"
40
- spec.add_development_dependency "rspec", "~> 3.0"
40
+ spec.add_development_dependency "rspec", ">= 3.0"
41
+ spec.add_development_dependency "rspec-rails"
42
+ spec.add_development_dependency "sqlite3"
43
+ spec.add_development_dependency 'rails'
44
+ spec.add_development_dependency 'byebug'
41
45
  spec.add_dependency "activesupport", ">= 3.0.0"
42
46
  end
@@ -1,6 +1,14 @@
1
1
  require "json_api_filter/version"
2
- require "json_api_filter/filters"
3
- require "json_api_filter/filter_by"
2
+ require "json_api_filter/auto_join"
3
+ require "json_api_filter/dispatch"
4
+ require "json_api_filter/filter_attributes"
5
+ require "json_api_filter/value_parser"
6
+ require "json_api_filter/field_filters/base"
7
+ require "json_api_filter/field_filters/matcher"
8
+ require "json_api_filter/field_filters/compare"
9
+ require "json_api_filter/field_filters/searcher"
10
+ require "json_api_filter/field_filters/sorter"
11
+ require "json_api_filter/field_filters/pagination"
4
12
  require "active_support/concern"
5
13
  require "active_support/core_ext/object/blank"
6
14
 
@@ -13,16 +21,19 @@ module JsonApiFilter
13
21
 
14
22
  extend ::ActiveSupport::Concern
15
23
  included do
16
-
17
- def attr_filter(query_params = params)
24
+
25
+ # @param [ActiveRecord::Base] scope
26
+ def json_api_filter(scope, query_params = params)
18
27
  unless self.class.json_api_permitted_filters.present?
19
28
  raise ::JsonApiFilter::MissingPermittedFilterError
20
29
  end
21
30
 
22
- ::JsonApiFilter::Filters.new(
23
- self.class.json_api_permitted_filters,
24
- query_params
25
- ).to_hash
31
+ ::JsonApiFilter::Dispatch.new(
32
+ scope,
33
+ query_params,
34
+ allowed_filters: self.class.json_api_permitted_filters,
35
+ allowed_searches: self.class.json_api_permitted_searches
36
+ ).process
26
37
  end
27
38
 
28
39
  def self.permitted_filters(val)
@@ -34,5 +45,15 @@ module JsonApiFilter
34
45
  def self.json_api_permitted_filters
35
46
  []
36
47
  end
48
+
49
+ def self.permitted_searches(global, **columns)
50
+ define_singleton_method(:json_api_permitted_searches) do
51
+ { global: global, columns: columns }
52
+ end
53
+ end
54
+
55
+ def self.json_api_permitted_searches
56
+ {}
57
+ end
37
58
  end
38
59
  end
@@ -0,0 +1,18 @@
1
+ module JsonApiFilter
2
+ class AutoJoin
3
+
4
+ attr_reader :association_name, :scope
5
+
6
+ # @param [ActiveRecord::Base, ActiveRecord_Relation] scope
7
+ # @param [String, Symbol] association_name
8
+ def initialize(scope, association_name)
9
+ @association_name = association_name
10
+ @scope = scope
11
+ end
12
+
13
+ def predicate
14
+ scope.joins(association_name.to_sym)
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,112 @@
1
+ module JsonApiFilter
2
+ class Dispatch
3
+
4
+ attr_reader :params, :scope, :allowed_filters, :allowed_searches
5
+
6
+ # @param [ActiveRecord::Base] scope
7
+ # @param [Hash, ActionController::Parameters] params
8
+ # @param [Array<Symbol>] allowed_filters
9
+ def initialize(scope, params, allowed_filters:, allowed_searches:)
10
+ @params = params
11
+ @scope = scope
12
+ @allowed_filters = allowed_filters
13
+ @allowed_searches = allowed_searches
14
+ end
15
+
16
+ # @return [ActiveRecord_Relation]
17
+ def process
18
+ @scope = [
19
+ scope.all,
20
+ sort_predicate,
21
+ filters_predicate,
22
+ search_predicate,
23
+ join_predicate,
24
+ ].compact.reduce(&:merge)
25
+ return scope if params[:pagination].nil?
26
+ scope.merge(pagination_predicate)
27
+ end
28
+
29
+ private
30
+
31
+ # @return [ActiveRecord::Base, NilClass]
32
+ def filters_predicate
33
+ #TODO: split this method
34
+ parser_params.fetch('filter', {}).map do |key, value|
35
+ next unless filters.include?(key)
36
+
37
+ if nested_filters.include?(key.to_sym)
38
+ next ::JsonApiFilter::FieldFilters::Matcher.new(
39
+ scope,
40
+ {key => value},
41
+ association: true
42
+ )
43
+ end
44
+
45
+ if value.class != ActiveSupport::HashWithIndifferentAccess
46
+ next ::JsonApiFilter::FieldFilters::Matcher.new(scope, {key => value})
47
+ end
48
+
49
+ ::JsonApiFilter::FieldFilters::Compare.new(
50
+ scope,
51
+ {key => value},
52
+ allowed_searches: allowed_searches
53
+ )
54
+ end.compact.map(&:predicate).reduce(&:merge)
55
+ end
56
+
57
+ # @return [ActiveRecord::Base, NilClass]
58
+ def sort_predicate
59
+ sort = parser_params[:sort]
60
+ return nil if sort.blank?
61
+
62
+ ::JsonApiFilter::FieldFilters::Sorter.new(scope, sort).predicate
63
+ end
64
+
65
+ # @return [ActiveRecord::Base, NilClass]
66
+ def search_predicate
67
+ return nil if parser_params[:search].blank?
68
+ return nil if allowed_searches[:global].nil?
69
+
70
+ ::JsonApiFilter::FieldFilters::Searcher.new(
71
+ scope,
72
+ {allowed_searches[:global] => parser_params[:search]}
73
+ ).predicate
74
+ end
75
+
76
+ # @return [ActiveRecord::Base, NilClass]
77
+ def pagination_predicate
78
+ return nil if parser_params[:pagination].nil?
79
+
80
+ ::JsonApiFilter::FieldFilters::Pagination.new(
81
+ scope,
82
+ parser_params[:pagination]
83
+ ).predicate
84
+ end
85
+
86
+ # @return [ActiveRecord::Base, NilClass]
87
+ def join_predicate
88
+ parser_params.fetch('filter', {}).map do |key, value|
89
+ next unless nested_filters.include?(key.to_sym)
90
+
91
+ ::JsonApiFilter::AutoJoin.new(scope, key)
92
+ end.compact.map(&:predicate).reduce(&:merge)
93
+ end
94
+
95
+ # @return [Hash]
96
+ def parser_params
97
+ return params.to_unsafe_h if params.class == ActionController::Parameters
98
+
99
+ params
100
+ end
101
+
102
+ # @return [Hash]
103
+ def filters
104
+ FilterAttributes.new(allowed_filters, parser_params[:filter]).process
105
+ end
106
+
107
+ def nested_filters
108
+ FilterAttributes.new(allowed_filters, parser_params[:filter]).nested_allowed_filter
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,18 @@
1
+ module JsonApiFilter
2
+ module FieldFilters
3
+ class Base
4
+
5
+ attr_reader :scope, :values, :association
6
+
7
+ # @param [ActiveRecord::Base] scope
8
+ # @param [Hash] values
9
+ # @param [Boolean] association
10
+ def initialize(scope, values, association: false )
11
+ @scope = scope
12
+ @values = values
13
+ @association = association
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+
2
+ module JsonApiFilter
3
+ module FieldFilters
4
+ class Compare < Base
5
+
6
+ attr_reader :allowed_searches
7
+
8
+ def initialize(scope, values, allowed_searches:)
9
+ super(scope, values)
10
+ @allowed_searches = allowed_searches
11
+ end
12
+
13
+ # @return [ActiveRecord_Relation]
14
+ def predicate
15
+ column = values.keys.first
16
+ filter = values.first[1]
17
+ filter.map do |key, value|
18
+ if !WHERE_METHODS[key.to_sym].nil?
19
+ compare(column, key, value)
20
+ elsif key == "search"
21
+ next unless allowed_searches[:columns].keys.include?(column.to_sym)
22
+ ::JsonApiFilter::FieldFilters::Searcher.new(
23
+ scope,
24
+ {allowed_searches[:columns][column.to_sym] => value}
25
+ ).predicate
26
+ else
27
+ nil
28
+ end
29
+ end.compact.reduce(&:merge)
30
+ end
31
+
32
+ private
33
+
34
+ WHERE_METHODS = {
35
+ eq: "=",
36
+ ne: "!=",
37
+ gt: ">",
38
+ ge: ">=",
39
+ lt: "<",
40
+ le: "<=",
41
+ }
42
+
43
+ def compare(column, method, value)
44
+ if WHERE_METHODS[method.to_sym] == "="
45
+ # prefer this method for eq as it implicitely transforms enum into their integer representation
46
+ scope.where(column => value)
47
+ else
48
+ scope.where("#{column} #{WHERE_METHODS[method.to_sym]} ?", value)
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module JsonApiFilter
2
+ module FieldFilters
3
+ class Matcher < Base
4
+
5
+ # @return [ActiveRecord_Relation]
6
+ def predicate
7
+
8
+ values.map do |key, value|
9
+ scope.where(
10
+ key_translation(key) => ::JsonApiFilter::ValueParser.parse(value)
11
+ )
12
+ end.reduce(&:merge)
13
+ end
14
+
15
+ private
16
+
17
+ #TODO: move to another class
18
+ def key_translation(key)
19
+ return key unless association
20
+
21
+ table_name(key)
22
+ end
23
+
24
+ # @return [ActiveRecord::Base]
25
+ def model_klass
26
+ scope.try(:klass) || scope
27
+ end
28
+
29
+ def table_name(association_name)
30
+ model_klass.reflections[association_name.to_s].table_name
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ module JsonApiFilter
2
+ module FieldFilters
3
+ class Pagination < Base
4
+
5
+ # @return [ActiveRecord_Relation]
6
+ def predicate
7
+ page = values["page"]
8
+ per_page = values["perPage"]
9
+ result = scope
10
+ unless page.nil? || per_page.nil? || per_page == "-1"
11
+ result = result.limit(per_page.to_i)
12
+ result = result.offset((page.to_i - 1) * per_page.to_i)
13
+ end
14
+ result
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module JsonApiFilter
2
+ module FieldFilters
3
+ class Searcher < Base
4
+
5
+ # @return [ActiveRecord_Relation]
6
+ def predicate
7
+ scope.send(values.keys.first, values.values.first)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module JsonApiFilter
2
+ module FieldFilters
3
+ class Sorter < Base
4
+
5
+ # @return [ActiveRecord_Relation]
6
+ def predicate
7
+ return nil if values["by"].nil?
8
+ scope.order(values["by"] => order)
9
+ end
10
+
11
+ private
12
+
13
+ def order
14
+ ActiveModel::Type::Boolean.new.cast(values["desc"]) ? :desc : :asc
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ module JsonApiFilter
2
+ class FilterAttributes
3
+ attr_reader :allowed_filters, :params
4
+
5
+
6
+ # @param [ActiveSupport::HashWithIndifferentAccess, Hash] allowed_filters
7
+ # @param [ActiveSupport::HashWithIndifferentAccess, Hash] params
8
+ def initialize(allowed_filters, params)
9
+ @allowed_filters = allowed_filters
10
+ @params = params
11
+ end
12
+
13
+ # @return [ActiveSupport::HashWithIndifferentAccess, Hash]
14
+ def process
15
+ params.select do |k,v|
16
+ allowed_filters.include?(k.to_sym) ||
17
+ allowed_filters.include?(k.to_s) ||
18
+ nested_allowed_filter.include?(k.to_sym) ||
19
+ nested_allowed_filter.include?(k.to_s)
20
+ end
21
+ end
22
+
23
+ # Keys of nested allowed filters
24
+ # only one level of nesting supported
25
+ #
26
+ # @example
27
+ # for: permitted_filters [:id, :author, :name, users: [:id]]
28
+ # it returns: :users
29
+ #
30
+ # @return [Array<Symbol>]
31
+ def nested_allowed_filter
32
+ allowed_filters.select{|f| f.class == Hash}.map(&:keys).flatten
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,37 @@
1
+ module JsonApiFilter
2
+ class ValueParser
3
+
4
+ attr_reader :value
5
+
6
+ # @param [String, Hash, HashWithIndifferentAccess] value
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ # Transform string of coma separated values to Array
12
+ #
13
+ # @example
14
+ # "1,2,3" #=> [1,2,3]
15
+ #
16
+ # @param [String, Hash, HashWithIndifferentAccess] value
17
+ # @return [Array]
18
+ def self.parse(value)
19
+ new(value).indifferent_parse
20
+ end
21
+
22
+ def indifferent_parse
23
+ return parse if value.is_a? String
24
+ parse_hash if value.is_a? HashWithIndifferentAccess
25
+ end
26
+
27
+ def parse
28
+ return [value] unless value.include?(",")
29
+ value.split(',')
30
+ end
31
+
32
+ def parse_hash
33
+ value.map{|k,v| {k => self.class.parse(v)}}.reduce(&:merge)
34
+ end
35
+
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module JsonApiFilter
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_api_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blaked
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-25 00:00:00.000000000 Z
11
+ date: 2021-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -42,16 +42,72 @@ dependencies:
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: activesupport
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -86,8 +142,16 @@ files:
86
142
  - bin/setup
87
143
  - json_api_filter.gemspec
88
144
  - lib/json_api_filter.rb
89
- - lib/json_api_filter/filter_by.rb
90
- - lib/json_api_filter/filters.rb
145
+ - lib/json_api_filter/auto_join.rb
146
+ - lib/json_api_filter/dispatch.rb
147
+ - lib/json_api_filter/field_filters/base.rb
148
+ - lib/json_api_filter/field_filters/compare.rb
149
+ - lib/json_api_filter/field_filters/matcher.rb
150
+ - lib/json_api_filter/field_filters/pagination.rb
151
+ - lib/json_api_filter/field_filters/searcher.rb
152
+ - lib/json_api_filter/field_filters/sorter.rb
153
+ - lib/json_api_filter/filter_attributes.rb
154
+ - lib/json_api_filter/value_parser.rb
91
155
  - lib/json_api_filter/version.rb
92
156
  homepage: https://github.com/Blaked84/json_api_filter
93
157
  licenses:
@@ -1,29 +0,0 @@
1
- module JsonApiFilter
2
- class FilterBy
3
-
4
- attr_reader :attr, :value
5
-
6
- def initialize(attr, value)
7
- @attr = attr
8
- @value = value
9
- end
10
-
11
- def to_hash
12
- return {} unless parsed_value.present?
13
-
14
- { key.to_sym => parsed_value }
15
- end
16
-
17
- private
18
-
19
- def key
20
- attr.to_sym
21
- end
22
-
23
- def parsed_value
24
- value.split(',')
25
- end
26
- end
27
- end
28
-
29
-
@@ -1,24 +0,0 @@
1
- module JsonApiFilter
2
- class Filters
3
- attr_reader :allowed_filters, :params
4
-
5
- def initialize(allowed_filters, params)
6
- @allowed_filters = allowed_filters
7
- @params = params
8
- end
9
-
10
- def to_hash
11
- filters.inject(:merge)
12
- end
13
-
14
- private
15
-
16
- def filters
17
- allowed_filters.map do |attr|
18
- value = params.fetch(:filter, {}).fetch(attr, "")
19
- ::JsonApiFilter::FilterBy.new(attr, value).to_hash
20
- end
21
- end
22
- end
23
- end
24
-