rokaki 0.8.1.2 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f371c83c5d42bd91e4f51363b7ec474807fceb3fb94dc7f9bd76bd067554d167
4
- data.tar.gz: 373f24755a5f2bd886aa56575b1f60f475db07db6a67b4716ed419a0896996eb
3
+ metadata.gz: 7b14c1377cb6ea9d8aa3571f310ffc884f027c495067e08adfce02caf1f7d0d2
4
+ data.tar.gz: 5326391f9ed786eb26f9cb5573d2afc78862e72b5ab64f8b88f26b8ff18fdcfe
5
5
  SHA512:
6
- metadata.gz: 200432e50f8d847fa7203760b7f52cbfd6743dfdb4247388dfe5e636b79331acec739759fe65b52ebde23585eb90ffbada5802cf5be2f19431410bd612c084f3
7
- data.tar.gz: 49207149db07210dc0c55fdfbf976737a404e4c255acff2484347b53339d8b18d2ed9f6bd0ca3a5efc2a79b583bf4f6489583cbc586ecce30aa1bde498201881
6
+ metadata.gz: a5c2e4a0b0fea5a627db0136291654aa6df25e6a2f548ba3c42fc1ebc8b3e427c3fcc53e0488c1df6e99f8bb6fd44e752c3ccdabe1f4be506a5fd7d53878cf3c
7
+ data.tar.gz: 79934522b0dba27889946208f47a8b2d769362016ae914ce99c7796c0b6899f6de8d96e25569e9ecc5086f0b0aeae23f0a6e21b6fe410db022eb89d6c6c15dd7
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
  tags
13
13
  *.gem
14
+ TODO.md
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.4
data/Gemfile.lock CHANGED
@@ -1,38 +1,42 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.8.1.2)
4
+ rokaki (0.8.3.1)
5
5
  activesupport
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.0.3.2)
11
- activesupport (= 6.0.3.2)
12
- activerecord (6.0.3.2)
13
- activemodel (= 6.0.3.2)
14
- activesupport (= 6.0.3.2)
15
- activesupport (6.0.3.2)
10
+ activemodel (6.1.3)
11
+ activesupport (= 6.1.3)
12
+ activerecord (6.1.3)
13
+ activemodel (= 6.1.3)
14
+ activesupport (= 6.1.3)
15
+ activesupport (6.1.3)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
- i18n (>= 0.7, < 2)
18
- minitest (~> 5.1)
19
- tzinfo (~> 1.1)
20
- zeitwerk (~> 2.2, >= 2.2.2)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
20
+ zeitwerk (~> 2.3)
21
21
  byebug (11.1.3)
22
22
  coderay (1.1.3)
23
- concurrent-ruby (1.1.6)
23
+ concurrent-ruby (1.1.8)
24
+ database_cleaner (1.8.5)
25
+ database_cleaner-active_record (1.8.0)
26
+ activerecord
27
+ database_cleaner (~> 1.8.0)
24
28
  diff-lcs (1.3)
25
29
  factory_bot (6.0.2)
26
30
  activesupport (>= 5.0.0)
27
- ffi (1.13.1)
28
- formatador (0.2.5)
29
- guard (2.16.2)
31
+ ffi (1.15.3)
32
+ formatador (0.3.0)
33
+ guard (2.18.0)
30
34
  formatador (>= 0.2.4)
31
35
  listen (>= 2.7, < 4.0)
32
36
  lumberjack (>= 1.0.12, < 2.0)
33
37
  nenv (~> 0.1)
34
38
  notiffany (~> 0.0)
35
- pry (>= 0.9.12)
39
+ pry (>= 0.13.0)
36
40
  shellany (~> 0.0)
37
41
  thor (>= 0.18.1)
38
42
  guard-compat (1.2.1)
@@ -40,14 +44,14 @@ GEM
40
44
  guard (~> 2.1)
41
45
  guard-compat (~> 1.1)
42
46
  rspec (>= 2.99.0, < 4.0)
43
- i18n (1.8.3)
47
+ i18n (1.8.9)
44
48
  concurrent-ruby (~> 1.0)
45
- listen (3.2.1)
49
+ listen (3.5.1)
46
50
  rb-fsevent (~> 0.10, >= 0.10.3)
47
51
  rb-inotify (~> 0.9, >= 0.9.10)
48
- lumberjack (1.2.6)
52
+ lumberjack (1.2.8)
49
53
  method_source (1.0.0)
50
- minitest (5.14.1)
54
+ minitest (5.14.4)
51
55
  nenv (0.3.0)
52
56
  notiffany (0.1.3)
53
57
  nenv (~> 0.1)
@@ -60,7 +64,7 @@ GEM
60
64
  byebug (~> 11.0)
61
65
  pry (~> 0.13.0)
62
66
  rake (13.0.1)
63
- rb-fsevent (0.10.4)
67
+ rb-fsevent (0.11.0)
64
68
  rb-inotify (0.10.1)
65
69
  ffi (~> 1.0)
66
70
  rspec (3.9.0)
@@ -78,11 +82,10 @@ GEM
78
82
  rspec-support (3.9.3)
79
83
  shellany (0.0.1)
80
84
  sqlite3 (1.4.2)
81
- thor (1.0.1)
82
- thread_safe (0.3.6)
83
- tzinfo (1.2.7)
84
- thread_safe (~> 0.1)
85
- zeitwerk (2.3.0)
85
+ thor (1.1.0)
86
+ tzinfo (2.0.4)
87
+ concurrent-ruby (~> 1.0)
88
+ zeitwerk (2.4.2)
86
89
 
87
90
  PLATFORMS
88
91
  ruby
@@ -90,6 +93,7 @@ PLATFORMS
90
93
  DEPENDENCIES
91
94
  activerecord
92
95
  bundler (~> 2.0)
96
+ database_cleaner-active_record
93
97
  factory_bot
94
98
  guard
95
99
  guard-rspec
@@ -102,4 +106,4 @@ DEPENDENCIES
102
106
  sqlite3
103
107
 
104
108
  BUNDLED WITH
105
- 2.1.4
109
+ 2.2.3
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Rokaki
2
+
2
3
  [![Gem Version](https://badge.fury.io/rb/rokaki.svg)](https://badge.fury.io/rb/rokaki)
3
4
 
4
5
  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".
@@ -46,7 +47,8 @@ class FilterArticles
46
47
 
47
48
  def filter_results
48
49
  @articles = @articles.where(date: date) if date
49
- @articles = @articles.joins(:author).where(author: { first_name: author_first_name }) if author_first_name
50
+ @articles = @articles.joins(:author).where(authors: { first_name: author_first_name }) if author_first_name
51
+ @articles = @articles.joins(:author).where(authors: { last_name: author_last_name }) if author_last_name
50
52
  end
51
53
  end
52
54
 
@@ -98,6 +100,7 @@ advanced_filterable.advanced__filter_key_4__deep_leaf_array == [1,2,3,4]
98
100
  advanced_filterable.advanced__filter_key_1__filter_key_3__deep_node == 'NODE'
99
101
  ```
100
102
  ### `#define_filter_map`
103
+ The define_filter_map method is more suited to classic "search", where you might want to search multiple fields on a model or across a graph. See the section on [filter_map](https://github.com/tevio/rokaki#2-the-filter_map-command-syntax) with OR for more on this kind of application.
101
104
 
102
105
  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.
103
106
 
@@ -115,7 +118,7 @@ class FilterMap
115
118
  define_filter_map :query, :mapped_a, association: :field
116
119
  end
117
120
 
118
- filter_map = FilterMap.new(fytlerz: { query: 'H2O' })
121
+ filter_map = FilterMap.new(fylterz: { query: 'H2O' })
119
122
 
120
123
  filter_map.mapped_a == 'H2O'
121
124
  filter_map.association_field = 'H2O'
@@ -227,6 +230,88 @@ filtered_authors = AuthorFilter.new(filters: filters).results
227
230
 
228
231
  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.
229
232
 
233
+ 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:-
234
+
235
+
236
+ ```ruby
237
+ class AuthorFilter
238
+ include Rokaki::FilterModel
239
+
240
+ filter_map :author, :query,
241
+ like: {
242
+ articles: {
243
+ title: :circumfix,
244
+ or: { # the or is aware of the join and will generate a compound join aware or query
245
+ reviews: {
246
+ title: :circumfix
247
+ }
248
+ }
249
+ },
250
+ }
251
+
252
+ attr_accessor :filters, :model
253
+
254
+ def initialize(filters:)
255
+ @filters = filters
256
+ end
257
+ end
258
+
259
+ filters = { query: "Lao" }
260
+ filtered_authors = AuthorFilter.new(filters: filters).results
261
+ ```
262
+
263
+ ## CAVEATS
264
+ Active record OR over a join may require you to add something like the following in an initializer in order for it to function properly:-
265
+
266
+ ### #structurally_incompatible_values_for_or
267
+
268
+ ``` ruby
269
+ module ActiveRecord
270
+ module QueryMethods
271
+ def structurally_incompatible_values_for_or(other)
272
+ Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } +
273
+ (Relation::MULTI_VALUE_METHODS - [:joins, :eager_load, :references, :extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } +
274
+ (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") }
275
+ end
276
+ end
277
+ end
278
+ ```
279
+
280
+ ### A has one relation to a model called Or
281
+ If you happen to have a model/table named 'Or' then you can override the `or:` key syntax by specifying a special `or_key`:-
282
+
283
+ ```ruby
284
+ class AuthorFilter
285
+ include Rokaki::FilterModel
286
+
287
+ or_key :my_or
288
+ filter_map :author, :query,
289
+ like: {
290
+ articles: {
291
+ title: :circumfix,
292
+ my_or: { # the or is aware of the join and will generate a compound join aware or query
293
+ or: { # The Or model has a title field
294
+ title: :circumfix
295
+ }
296
+ }
297
+ },
298
+ }
299
+
300
+ attr_accessor :filters, :model
301
+
302
+ def initialize(filters:)
303
+ @filters = filters
304
+ end
305
+ end
306
+
307
+ filters = { query: "Syntaxes" }
308
+ filtered_authors = AuthorFilter.new(filters: filters).results
309
+ ```
310
+
311
+
312
+ See [this issue](https://github.com/rails/rails/issues/24055) for details.
313
+
314
+
230
315
  #### 3. The porcelain command syntax
231
316
 
232
317
  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`
data/lib/rokaki.rb CHANGED
@@ -3,13 +3,12 @@
3
3
  require 'rokaki/version'
4
4
  require 'rokaki/filterable'
5
5
  require 'rokaki/filter_model'
6
+ require 'rokaki/filter_model/join_map'
6
7
  require 'rokaki/filter_model/like_keys'
7
8
  require 'rokaki/filter_model/basic_filter'
8
9
  require 'rokaki/filter_model/nested_filter'
10
+ require 'rokaki/filter_model/nested_like_filters'
9
11
 
10
12
  module Rokaki
11
13
  class Error < StandardError; end
12
-
13
- # include this module for filters dsl in an object
14
- #
15
14
  end
@@ -8,7 +8,7 @@ module Rokaki
8
8
  end
9
9
 
10
10
  def prepare_terms(param, mode)
11
- if param.is_a? Array
11
+ if Array === param
12
12
  return param.map { |term| "%#{term}%" } if mode == :circumfix
13
13
  return param.map { |term| "%#{term}" } if mode == :prefix
14
14
  return param.map { |term| "#{term}%" } if mode == :suffix
@@ -56,18 +56,39 @@ module Rokaki
56
56
 
57
57
  @_chain_filters ||= []
58
58
  filter_keys.each do |filter_key|
59
-
60
59
  # TODO: does the key need casting to an array here?
61
60
  _chain_filter(filter_key) unless filter_key.is_a? Hash
62
-
63
61
  _chain_nested_filter(filter_key) if filter_key.is_a? Hash
62
+ end
63
+
64
+ define_results # writes out all the generated filters
65
+ end
64
66
 
67
+ def like_filters(like_keys, term_type: :like)
68
+ if @filter_map_query_key
69
+ define_filter_map(@filter_map_query_key, *like_keys.call)
70
+ else
71
+ define_filter_keys(*like_keys.call)
65
72
  end
66
73
 
74
+ @_chain_filters ||= []
75
+ filter_map = []
76
+
77
+ nested_like_filter = NestedLikeFilters.new(
78
+ filter_key_object: like_keys,
79
+ prefix: filter_key_prefix,
80
+ infix: filter_key_infix,
81
+ db: @_filter_db,
82
+ type: term_type,
83
+ or_key: or_key
84
+ )
85
+ nested_like_filter.call
86
+
87
+ _chain_nested_like_filter(nested_like_filter)
67
88
  define_results # writes out all the generated filters
68
89
  end
69
90
 
70
- def _chain_filter(key)
91
+ def _build_basic_filter(key)
71
92
  basic_filter = BasicFilter.new(
72
93
  keys: [key],
73
94
  prefix: filter_key_prefix,
@@ -77,13 +98,17 @@ module Rokaki
77
98
  db: @_filter_db
78
99
  )
79
100
  basic_filter.call
101
+ basic_filter
102
+ end
80
103
 
104
+ def _chain_filter(key)
105
+ basic_filter = _build_basic_filter(key)
81
106
  class_eval basic_filter.filter_method, __FILE__, __LINE__ - 2
82
107
 
83
108
  @_chain_filters << basic_filter.filter_template
84
109
  end
85
110
 
86
- def _chain_nested_filter(filters_object)
111
+ def _build_nested_filter(filters_object)
87
112
  nested_filter = NestedFilter.new(
88
113
  filter_key_object: filters_object,
89
114
  prefix: filter_key_prefix,
@@ -93,6 +118,21 @@ module Rokaki
93
118
  db: @_filter_db
94
119
  )
95
120
  nested_filter.call
121
+ nested_filter
122
+ end
123
+
124
+ def _chain_nested_like_filter(filters_object)
125
+ filters_object.filter_methods.each do |filter_method|
126
+ class_eval filter_method, __FILE__, __LINE__ - 2
127
+ end
128
+
129
+ filters_object.templates.each do |filter_template|
130
+ @_chain_filters << filter_template
131
+ end
132
+ end
133
+
134
+ def _chain_nested_filter(filters_object)
135
+ nested_filter = _build_nested_filter(filters_object)
96
136
 
97
137
  nested_filter.filter_methods.each do |filter_method|
98
138
  class_eval filter_method, __FILE__, __LINE__ - 2
@@ -103,10 +143,6 @@ module Rokaki
103
143
  end
104
144
  end
105
145
 
106
- # def associated_table(association)
107
- # @model.reflect_on_association(association).klass.table_name
108
- # end
109
-
110
146
  def filter_model(model_class)
111
147
  @model = (model_class.is_a?(Class) ? model_class : Object.const_get(model_class.capitalize))
112
148
  class_eval "def set_model; @model ||= #{@model}; end;"
@@ -116,41 +152,16 @@ module Rokaki
116
152
  raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
117
153
  @_like_semantics = (@_like_semantics || {}).merge(args)
118
154
 
119
- key_builder = LikeKeys.new(args)
120
- keys = key_builder.call
121
-
122
- filters(*keys)
155
+ like_keys = LikeKeys.new(args)
156
+ like_filters(like_keys, term_type: :like)
123
157
  end
124
158
 
125
159
  def ilike(args)
126
160
  raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
127
161
  @i_like_semantics = (@i_like_semantics || {}).merge(args)
128
162
 
129
- key_builder = LikeKeys.new(args)
130
- keys = key_builder.call
131
-
132
- filters(*keys)
133
- end
134
-
135
- def deep_chain(keys, value)
136
- if value.is_a? Hash
137
- value.keys.map do |key|
138
- _keys = keys.dup << key
139
- deep_chain(_keys, value[key])
140
- end
141
- end
142
-
143
- if value.is_a? Array
144
- value.each do |av|
145
- _keys = keys.dup << av
146
- _build_deep_chain(_keys)
147
- end
148
- end
149
-
150
- if value.is_a? Symbol
151
- _keys = keys.dup << value
152
- _build_deep_chain(_keys)
153
- end
163
+ like_keys = LikeKeys.new(args)
164
+ like_filters(like_keys, term_type: :ilike)
154
165
  end
155
166
 
156
167
  # the model method is called to instatiate @model from the
@@ -10,8 +10,9 @@ module Rokaki
10
10
  @like_semantics = like_semantics
11
11
  @i_like_semantics = i_like_semantics
12
12
  @db = db
13
+ @filter_query = nil
13
14
  end
14
- attr_reader :keys, :prefix, :infix, :like_semantics, :i_like_semantics, :db
15
+ attr_reader :keys, :prefix, :infix, :like_semantics, :i_like_semantics, :db, :filter_query
15
16
  attr_accessor :filter_method, :filter_template
16
17
 
17
18
  def call
@@ -56,7 +57,7 @@ module Rokaki
56
57
  query = "@model.where(#{key}: #{filter})"
57
58
  end
58
59
 
59
- query
60
+ @filter_query = query
60
61
  end
61
62
 
62
63
  def build_like_query(type:, query:, filter:, mode:, key:)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rokaki
4
+ module FilterModel
5
+ class DeepAssignStruct
6
+ def initialize(keys:, value:, struct: nil)
7
+ @keys = keys
8
+ @value = value
9
+ @struct = struct
10
+ end
11
+ attr_reader :keys, :value
12
+ attr_accessor :struct
13
+
14
+ def call
15
+ base_keys = keys
16
+ i = base_keys.length - 1
17
+
18
+ base_keys.reverse_each.reduce (value) do |struc,key|
19
+ i -= 1
20
+ cur_keys = base_keys[0..i]
21
+
22
+ if struct
23
+ val = struct.dig(*cur_keys)
24
+ val[key] = struc
25
+ p val
26
+ return val
27
+ else
28
+ if key.is_a?(Integer)
29
+ struct = [struc]
30
+ else
31
+ { key=>struc }
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def deep_construct(keys, value)
40
+
41
+ if keys.last.is_a?(Integer)
42
+ rstruct = struct[keys.last] = value
43
+ else
44
+ rstruct = { keys.last => value }
45
+ end
46
+
47
+ keys[0..-2].reverse_each.reduce (rstruct) do |struc,key|
48
+ if key.is_a?(Integer)
49
+ [struc]
50
+ else
51
+ { key=>struc }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DOUBLE SPLAT HASHES TO MAKE ARG LISTS!
4
+ #
5
+ # Array#dig could be useful
6
+ #
7
+ # Array#intersection could be useful
8
+ #
9
+ # Array#difference could be useful
10
+ #
11
+
12
+ module Rokaki
13
+ module FilterModel
14
+ class JoinMap
15
+ def initialize(key_paths)
16
+ @key_paths = key_paths
17
+ @result = {}
18
+ end
19
+
20
+ attr_reader :key_paths
21
+ attr_accessor :result
22
+
23
+ def call
24
+ key_paths.uniq.each do |key_path|
25
+ current_key_path = []
26
+ previous_key = nil
27
+
28
+ if Symbol === key_path
29
+ if key_paths.length == 1
30
+ @result = key_paths
31
+ else
32
+ result[key_path] = {} unless result.keys.include? key_path
33
+ end
34
+ end
35
+
36
+ if Array === key_path
37
+ key_path.each do |key|
38
+ current_path_length = current_key_path.length
39
+
40
+ if current_path_length > 0 && result.dig(current_key_path).nil?
41
+
42
+ if current_path_length == 1
43
+ parent_result = result[previous_key]
44
+
45
+ if Symbol === parent_result && parent_result != key
46
+ result[previous_key] = [parent_result, key]
47
+ elsif Array === parent_result
48
+
49
+ parent_result.each_with_index do |array_item, index|
50
+ if array_item == key
51
+ current_key_path << index
52
+ end
53
+ end
54
+
55
+ else
56
+ result[previous_key] = key unless result[previous_key] == key
57
+ end
58
+
59
+ else
60
+ previous_key_path = current_key_path - [previous_key]
61
+ previous_path_length = previous_key_path.length
62
+ p current_key_path
63
+
64
+ if previous_path_length == 1
65
+ res = result.dig(*previous_key_path)
66
+
67
+ if Symbol === res
68
+ result[previous_key_path.first] = { previous_key => key }
69
+ end
70
+ elsif previous_path_length > 1
71
+ res = result.dig(*previous_key_path)
72
+
73
+ if Symbol === res
74
+ base = previous_key_path.pop
75
+ result.dig(*previous_key_path)[base] = { previous_key => key }
76
+ end
77
+ end
78
+
79
+ end
80
+ else
81
+ end
82
+
83
+ previous_key = key
84
+ current_key_path << key
85
+ end
86
+ end
87
+ end
88
+ result
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
@@ -9,43 +9,45 @@ module Rokaki
9
9
  class LikeKeys
10
10
  def initialize(args)
11
11
  @args = args
12
- @like_keys = []
12
+ @keys = []
13
+ @key_paths = []
13
14
  end
14
15
 
15
- attr_reader :args, :like_keys
16
+ attr_reader :args, :keys, :key_paths
16
17
 
17
18
  def call
18
19
  args.keys.each do |key|
19
- like_keys << map_keys(args[key], key)
20
+ map_keys(key: key, value: args[key])
20
21
  end
21
- like_keys
22
+ keys
22
23
  end
23
24
 
24
25
  private
25
26
 
26
- def map_keys(value, key)
27
- key_result = {}
27
+ def map_keys(key:, value:, key_path: [])
28
28
 
29
- if value.is_a? Hash
30
- value.keys.each do |sub_key|
31
- sub_value = value[sub_key]
32
-
33
- if sub_value.is_a? Symbol
34
- if key_result[key].is_a? Array
35
- key_result[key] << sub_key
36
- else
37
- key_result[key] = [ sub_key ]
38
- end
39
-
40
- elsif sub_value.is_a? Hash
41
- key_result[key] = map_keys(sub_value, sub_key)
29
+ if value.is_a?(Hash)
30
+ key_path << key
31
+ value.keys.each do |key|
32
+ map_keys(key: key, value: value[key], key_path: key_path.dup)
42
33
  end
43
34
  end
44
- else
45
- key_result = key
35
+
36
+ if value.is_a?(Symbol)
37
+ keys << (key_path.empty? ? key : deep_assign(key_path, key))
38
+ key_path << key
39
+ key_paths << key_path
46
40
  end
47
41
 
48
- key_result
42
+ key_path
43
+
44
+ end
45
+
46
+ # Many thanks Cary Swoveland
47
+ # https://stackoverflow.com/questions/56634950/ruby-dig-set-assign-values-using-hashdig/56635124
48
+ #
49
+ def deep_assign(keys, value)
50
+ keys[0..-2].reverse_each.reduce ({ keys.last => value }) { |h,key| { key=>h } }
49
51
  end
50
52
  end
51
53
  end
@@ -66,7 +66,7 @@ module Rokaki
66
66
  end
67
67
 
68
68
  def _build_deep_chain(keys)
69
- name = '' # prefix.to_s
69
+ name = ''
70
70
  count = keys.size - 1
71
71
 
72
72
  joins_before = []
@@ -75,13 +75,13 @@ module Rokaki
75
75
  where_before = []
76
76
  where_after = []
77
77
  out = ''
78
- mode = nil
78
+ search_mode = nil
79
79
  type = nil
80
80
  leaf = nil
81
81
 
82
- if mode = find_like_key(keys)
82
+ if search_mode = find_like_key(keys)
83
83
  type = 'LIKE'
84
- elsif mode = find_i_like_key(keys)
84
+ elsif search_mode = find_i_like_key(keys)
85
85
  type = 'ILIKE'
86
86
  end
87
87
  leaf = keys.pop
@@ -114,12 +114,12 @@ module Rokaki
114
114
  joins = joins.join
115
115
  where = where.join
116
116
 
117
- if mode
117
+ if search_mode
118
118
  query = build_like_query(
119
119
  type: type,
120
120
  query: '',
121
121
  filter: "#{prefix}#{name}",
122
- mode: mode,
122
+ search_mode: search_mode,
123
123
  key: keys.last.to_s.pluralize,
124
124
  leaf: leaf
125
125
  )
@@ -136,15 +136,15 @@ module Rokaki
136
136
  end
137
137
  end
138
138
 
139
- def build_like_query(type:, query:, filter:, mode:, key:, leaf:)
139
+ def build_like_query(type:, query:, filter:, search_mode:, key:, leaf:)
140
140
  if db == :postgres
141
141
  query = "where(\"#{key}.#{leaf} #{type} ANY (ARRAY[?])\", "
142
- query += "prepare_terms(#{filter}, :#{mode}))"
142
+ query += "prepare_terms(#{filter}, :#{search_mode}))"
143
143
  else
144
144
  query = "where(\"#{key}.#{leaf} #{type} :query\", "
145
- query += "query: \"%\#{#{filter}}%\")" if mode == :circumfix
146
- query += "query: \"%\#{#{filter}}\")" if mode == :prefix
147
- query += "query: \"\#{#{filter}}%\")" if mode == :suffix
145
+ query += "query: \"%\#{#{filter}}%\")" if search_mode == :circumfix
146
+ query += "query: \"%\#{#{filter}}\")" if search_mode == :prefix
147
+ query += "query: \"\#{#{filter}}%\")" if search_mode == :suffix
148
148
  end
149
149
 
150
150
  query
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/inflector'
3
+
4
+ module Rokaki
5
+ module FilterModel
6
+ class NestedLikeFilters
7
+ def initialize(filter_key_object:, prefix:, infix:, db:, mode: :and, or_key: :or, type: :like)
8
+ @filter_key_object = filter_key_object
9
+ @prefix = prefix
10
+ @infix = infix
11
+ @db = db
12
+ @mode = mode
13
+ @or_key = or_key
14
+ @type = type
15
+
16
+ @names = []
17
+ @filter_methods = []
18
+ @templates = []
19
+ @filter_queries = []
20
+ @method_names = []
21
+ @filter_names = []
22
+ @join_key_paths = []
23
+ @key_paths = []
24
+ @search_modes = []
25
+ @modes = []
26
+ end
27
+ attr_reader :filter_key_object, :prefix, :infix, :like_semantics, :i_like_semantics,
28
+ :db, :mode, :or_key, :filter_queries, :type
29
+ attr_accessor :filter_methods, :templates, :method_names, :filter_names, :names, :join_key_paths, :key_paths, :search_modes, :modes
30
+
31
+ def call
32
+ build_filters_data
33
+ compound_filters
34
+ end
35
+
36
+ def build_filters_data
37
+ results = filter_key_object.key_paths.each do |key_path|
38
+ if key_path.is_a?(Symbol)
39
+ build_filter_data(key_path)
40
+ else
41
+ if key_path.include? or_key
42
+ build_filter_data(key_path.dup, mode: or_key)
43
+ else
44
+ build_filter_data(key_path.dup)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def compound_filters
51
+ # key_paths represents a structure like
52
+ # [
53
+ # [ # this is an or
54
+ # [:articles, :title],
55
+ # [:articles, :authors, :first_name],
56
+ # [:articles, :authors, :reviews, :title],
57
+ # [:articles, :authors, :reviews, :content]
58
+ # ],
59
+ # [:articles, :content] # this is an and
60
+ # ]
61
+ #
62
+ # Each item in the array represents a compounded filter
63
+ #
64
+ key_paths.each_with_index do |key_path_item, index|
65
+ base_names = get_name(index)
66
+ join_map = JoinMap.new(join_key_paths[index])
67
+ join_map.call
68
+
69
+ if key_path_item.first.is_a?(Array)
70
+ item_search_modes = search_modes[index]
71
+
72
+ base_name = base_names.shift
73
+ method_name = prefix.to_s + ([:filter].push base_name).compact.join(infix.to_s)
74
+ method_name += (infix.to_s+'or'+infix.to_s) + (base_names).join(infix.to_s+'or'+infix.to_s)
75
+ item_filter_names = [prefix.to_s + base_name]
76
+
77
+ base_names.each do |filter_base_name|
78
+ item_filter_names << (prefix.to_s + filter_base_name)
79
+ end
80
+
81
+ base_modes = modes[index]
82
+ key_path_item.each_with_index do |key_path, kp_index|
83
+
84
+ build_query(keys: key_path.dup, join_map: join_map.result, mode: base_modes[kp_index], filter_name: item_filter_names[kp_index], search_mode: item_search_modes[kp_index])
85
+ end
86
+
87
+ item_filter_queries = filter_queries[index]
88
+ first_query = item_filter_queries.shift
89
+
90
+ ored = item_filter_queries.map do |query|
91
+ ".or(#{query})"
92
+ end
93
+
94
+ filter_conditions = item_filter_names.join(' || ')
95
+
96
+ @filter_methods << "def #{method_name}; #{first_query + ored.join}; end;"
97
+ @templates << "@model = #{method_name} if #{filter_conditions};"
98
+ else
99
+
100
+ base_name = get_name(index)
101
+ filter_name = "#{prefix}#{get_filter_name(index)}"
102
+
103
+ method_name = ([prefix, :filter, base_name]).compact.join(infix.to_s)
104
+
105
+ build_query(keys: key_path_item.dup, join_map: join_map.result, filter_name: filter_name, search_mode: search_modes[index])
106
+
107
+ @filter_methods << "def #{method_name}; #{filter_queries[index]}; end;"
108
+ @templates << "@model = #{method_name} if #{filter_name};"
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def get_name(index)
116
+ names[index]
117
+ end
118
+
119
+ def get_filter_name(index)
120
+ filter_names[index]
121
+ end
122
+
123
+ def find_mode_key(keys)
124
+ current_like_key = @filter_key_object.args.dup
125
+ keys.each do |key|
126
+ current_like_key = current_like_key[key]
127
+ end
128
+ current_like_key
129
+ end
130
+
131
+ def build_filter_data(key_path, mode: :and)
132
+ # if key_path.is_a?(Symbol)
133
+ # search_mode = @filter_key_object.args[key_path]
134
+
135
+ # name = key_path
136
+ # filter_name = (prefix.to_s + key_path.to_s)
137
+ # @names << name
138
+ # @filter_names << filter_name
139
+ # @key_paths << key_path
140
+ # @search_modes << search_mode
141
+ # @modes << mode
142
+ # else
143
+ search_mode = find_mode_key(key_path)
144
+
145
+ key_path.delete(mode)
146
+
147
+ name = key_path.join(infix.to_s)
148
+ filter_name = key_path.compact.join(infix.to_s)
149
+
150
+ if mode == or_key
151
+ @names << [@names.pop, name].flatten
152
+ @filter_names << [@filter_names.pop, filter_name].flatten
153
+
154
+ or_key_paths = @key_paths.pop
155
+ if or_key_paths.first.is_a?(Array)
156
+ @key_paths << [*or_key_paths] + [key_path.dup]
157
+ else
158
+ @key_paths << [or_key_paths] + [key_path.dup]
159
+ end
160
+
161
+ @search_modes << [@search_modes.pop, search_mode].flatten
162
+ @modes << [@modes.pop, mode].flatten
163
+
164
+ else
165
+ @names << name
166
+ @filter_names << filter_name
167
+ @key_paths << key_path.dup # having this wrapped in an array is messy for single items
168
+ @search_modes << search_mode
169
+ @modes << mode
170
+ end
171
+
172
+ join_key_path = key_path.dup
173
+
174
+ leaf = join_key_path.pop
175
+ if mode == or_key
176
+ or_join_key_paths = @join_key_paths.pop
177
+ if or_join_key_paths.first.is_a?(Array)
178
+ @join_key_paths << [*or_join_key_paths] + [join_key_path.dup]
179
+ else
180
+ @join_key_paths << [or_join_key_paths] + [join_key_path.dup]
181
+ end
182
+ else
183
+ if join_key_path.length == 1
184
+ @join_key_paths << join_key_path
185
+ else
186
+ @join_key_paths << [join_key_path.dup]
187
+ end
188
+ end
189
+ # end
190
+ end
191
+
192
+ # DOUBLE SPLAT HASHES TO MAKE ARG LISTS!
193
+ def build_query(keys: , join_map:, mode: :and, filter_name:, search_mode:)
194
+ leaf = nil
195
+ leaf = keys.pop
196
+
197
+
198
+ query = build_like_query(
199
+ type: type,
200
+ query: '',
201
+ filter: filter_name,
202
+ search_mode: search_mode,
203
+ key: keys.last,
204
+ leaf: leaf
205
+ )
206
+
207
+ if join_map.empty?
208
+ filter_query = "@model.#{query}"
209
+ elsif join_map.is_a?(Array)
210
+ filter_query = "@model.joins(*#{join_map}).#{query}"
211
+ else
212
+ filter_query = "@model.joins(**#{join_map}).#{query}"
213
+ end
214
+
215
+ if mode == or_key
216
+ @filter_queries << [@filter_queries.pop, filter_query].flatten
217
+ else
218
+ @filter_queries << filter_query
219
+ end
220
+ filter_query
221
+ end
222
+
223
+ def build_like_query(type:, query:, filter:, search_mode:, key:, leaf:)
224
+ key_leaf = key ? "#{key.to_s.pluralize}.#{leaf}" : leaf
225
+ if db == :postgres
226
+ query = "where(\"#{key_leaf} #{type.to_s.upcase} ANY (ARRAY[?])\", "
227
+ query += "prepare_terms(#{filter}, :#{search_mode}))"
228
+ else
229
+ query = "where(\"#{key_leaf} #{type.to_s.upcase} :query\", "
230
+ query += "query: \"%\#{#{filter}}%\")" if search_mode == :circumfix
231
+ query += "query: \"%\#{#{filter}}\")" if search_mode == :prefix
232
+ query += "query: \"\#{#{filter}}%\")" if search_mode == :suffix
233
+ end
234
+
235
+ query
236
+ end
237
+
238
+ end
239
+ end
240
+ end
241
+
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rokaki
4
+ # include this module for rokaki's filtering dsl in any object
5
+ #
2
6
  module Filterable
3
7
  def self.included(base)
4
8
  base.extend(ClassMethods)
@@ -11,15 +15,15 @@ module Rokaki
11
15
 
12
16
  def define_filter_keys(*filter_keys)
13
17
  filter_keys.each do |filter_key|
14
- _build_filter([filter_key]) unless filter_key.is_a? Hash
15
- _nested_key filter_key if filter_key.is_a? Hash
18
+ _build_filter([filter_key]) unless Hash === filter_key
19
+ _nested_key filter_key if Hash === filter_key
16
20
  end
17
21
  end
18
22
 
19
23
  def define_filter_map(query_field, *filter_keys)
20
24
  filter_keys.each do |filter_key|
21
- _map_filters(query_field, [filter_key]) unless filter_key.is_a? Hash
22
- _nested_map query_field, filter_key if filter_key.is_a? Hash
25
+ _map_filters(query_field, [filter_key]) unless Hash === filter_key
26
+ _nested_map query_field, filter_key if Hash === filter_key
23
27
  end
24
28
  end
25
29
 
@@ -35,11 +39,16 @@ module Rokaki
35
39
  @filter_key_infix ||= infix
36
40
  end
37
41
 
42
+ def or_key(or_key = :or)
43
+ @or_key ||= or_key
44
+ end
45
+
38
46
  def filterable_object_name(name = 'filters')
39
47
  @filterable_object_name ||= name
40
48
  end
41
49
 
42
50
  def _build_filter(keys)
51
+ keys.delete(or_key)
43
52
  name = @filter_key_prefix.to_s
44
53
  count = keys.size - 1
45
54
 
@@ -52,6 +61,7 @@ module Rokaki
52
61
  end
53
62
 
54
63
  def _map_filters(query_field, keys)
64
+ keys.delete(or_key)
55
65
  name = @filter_key_prefix.to_s
56
66
  count = keys.size - 1
57
67
 
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.8.1.2"
2
+ VERSION = "0.8.4"
3
3
  end
data/rokaki.gemspec CHANGED
@@ -34,13 +34,17 @@ Gem::Specification.new do |spec|
34
34
 
35
35
  spec.add_development_dependency 'activerecord'
36
36
  spec.add_development_dependency 'bundler', '~> 2.0'
37
- spec.add_development_dependency 'factory_bot'
38
- spec.add_development_dependency 'guard'
39
- spec.add_development_dependency 'guard-rspec'
40
- spec.add_development_dependency 'pg'
41
37
  spec.add_development_dependency 'pry'
42
38
  spec.add_development_dependency 'pry-byebug'
43
39
  spec.add_development_dependency 'rake', '~> 13.0'
40
+
41
+ spec.add_development_dependency 'guard'
42
+ spec.add_development_dependency 'guard-rspec'
44
43
  spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'factory_bot'
45
+
46
+ spec.add_development_dependency 'pg'
45
47
  spec.add_development_dependency 'sqlite3'
48
+ spec.add_development_dependency 'database_cleaner-active_record'
49
+
46
50
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rokaki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1.2
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Martin
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-21 00:00:00.000000000 Z
11
+ date: 2021-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: factory_bot
56
+ name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: guard
70
+ name: pry-byebug
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,21 +81,21 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: guard-rspec
84
+ name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '13.0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '13.0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: pg
98
+ name: guard
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: pry
112
+ name: guard-rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -123,7 +123,21 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: pry-byebug
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: factory_bot
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - ">="
@@ -137,35 +151,35 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: rake
154
+ name: pg
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
- - - "~>"
157
+ - - ">="
144
158
  - !ruby/object:Gem::Version
145
- version: '13.0'
159
+ version: '0'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
- - - "~>"
164
+ - - ">="
151
165
  - !ruby/object:Gem::Version
152
- version: '13.0'
166
+ version: '0'
153
167
  - !ruby/object:Gem::Dependency
154
- name: rspec
168
+ name: sqlite3
155
169
  requirement: !ruby/object:Gem::Requirement
156
170
  requirements:
157
- - - "~>"
171
+ - - ">="
158
172
  - !ruby/object:Gem::Version
159
- version: '3.0'
173
+ version: '0'
160
174
  type: :development
161
175
  prerelease: false
162
176
  version_requirements: !ruby/object:Gem::Requirement
163
177
  requirements:
164
- - - "~>"
178
+ - - ">="
165
179
  - !ruby/object:Gem::Version
166
- version: '3.0'
180
+ version: '0'
167
181
  - !ruby/object:Gem::Dependency
168
- name: sqlite3
182
+ name: database_cleaner-active_record
169
183
  requirement: !ruby/object:Gem::Requirement
170
184
  requirements:
171
185
  - - ">="
@@ -187,6 +201,7 @@ extra_rdoc_files: []
187
201
  files:
188
202
  - ".gitignore"
189
203
  - ".rspec"
204
+ - ".ruby-version"
190
205
  - ".travis.yml"
191
206
  - CODE_OF_CONDUCT.md
192
207
  - Gemfile
@@ -200,9 +215,12 @@ files:
200
215
  - lib/rokaki.rb
201
216
  - lib/rokaki/filter_model.rb
202
217
  - lib/rokaki/filter_model/basic_filter.rb
218
+ - lib/rokaki/filter_model/deep_assign_struct.rb
203
219
  - lib/rokaki/filter_model/filter_chain.rb
220
+ - lib/rokaki/filter_model/join_map.rb
204
221
  - lib/rokaki/filter_model/like_keys.rb
205
222
  - lib/rokaki/filter_model/nested_filter.rb
223
+ - lib/rokaki/filter_model/nested_like_filters.rb
206
224
  - lib/rokaki/filterable.rb
207
225
  - lib/rokaki/version.rb
208
226
  - rokaki.gemspec
@@ -211,7 +229,7 @@ licenses:
211
229
  - MIT
212
230
  metadata:
213
231
  homepage_uri: https://github.com/tevio/rokaki
214
- post_install_message:
232
+ post_install_message:
215
233
  rdoc_options: []
216
234
  require_paths:
217
235
  - lib
@@ -226,8 +244,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
226
244
  - !ruby/object:Gem::Version
227
245
  version: '0'
228
246
  requirements: []
229
- rubygems_version: 3.1.2
230
- signing_key:
247
+ rubygems_version: 3.1.6
248
+ signing_key:
231
249
  specification_version: 4
232
250
  summary: A web request filtering library
233
251
  test_files: []