rokaki 0.8.1 → 0.8.3.1

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: 8a4312a80ff21e5c59187f2677c803626aa2d9bd2331316dea46de60461c50fa
4
- data.tar.gz: f2d1b2b2aeefb8f82cf7be3ecfe5f058472701ad23064a8909fe09e6c289e4d7
3
+ metadata.gz: 8f2a62792073dd9728a44158d6f30dc4f0f8119316bda3a3846c16fc2abc57f7
4
+ data.tar.gz: 80e9bfc7b677b7e291996c5bda1fc6652ddfbb592b31083446ef992755f00404
5
5
  SHA512:
6
- metadata.gz: c4d9d19c3d0f008721cfe2cbae3364f29e46fc094edd03af7738999a5cbe3680606f9201d884d4ce1316d5ad3da54b1f572d8bec7bcc3a00cc60696e36a0db11
7
- data.tar.gz: 2c83644c0338d9024422de614edb92d336ec542261f3453b07f5713e676e8c0c6c5947e8e75fb85bfde6f862185f56a9eac44664ea6769767dda2e901937b9ea
6
+ metadata.gz: 6570c3b380c66092b3a583b1c94a51f8364f06d936522cc72676e23c3aa1717b4b89cc65c0d3ed6e6d67291b3c7e1c3c83d8254acd7df8f9971700b314a71aa0
7
+ data.tar.gz: 913e7bf304da343a4dce4d061dbcdaa37c2f26d7a396e590e393a224a4ac7c30c439f8d085de1a865828c9931f1bcf99dc66b32c8494e8481dcc6c5539d03ea6
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
  tags
13
13
  *.gem
14
+ TODO.md
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.8.1)
4
+ rokaki (0.8.3.1)
5
5
  activesupport
6
6
 
7
7
  GEM
@@ -21,6 +21,10 @@ GEM
21
21
  byebug (11.1.3)
22
22
  coderay (1.1.3)
23
23
  concurrent-ruby (1.1.6)
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)
@@ -90,6 +94,7 @@ PLATFORMS
90
94
  DEPENDENCIES
91
95
  activerecord
92
96
  bundler (~> 2.0)
97
+ database_cleaner-active_record
93
98
  factory_bot
94
99
  guard
95
100
  guard-rspec
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # Rokaki
2
+ ## You know, for air.
3
+
2
4
  [![Gem Version](https://badge.fury.io/rb/rokaki.svg)](https://badge.fury.io/rb/rokaki)
3
5
 
4
6
  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 +48,8 @@ class FilterArticles
46
48
 
47
49
  def filter_results
48
50
  @articles = @articles.where(date: date) if date
49
- @articles = @articles.joins(:author).where(author: { first_name: author_first_name }) if author_first_name
51
+ @articles = @articles.joins(:author).where(authors: { first_name: author_first_name }) if author_first_name
52
+ @articles = @articles.joins(:author).where(authors: { last_name: author_last_name }) if author_last_name
50
53
  end
51
54
  end
52
55
 
@@ -98,6 +101,7 @@ advanced_filterable.advanced__filter_key_4__deep_leaf_array == [1,2,3,4]
98
101
  advanced_filterable.advanced__filter_key_1__filter_key_3__deep_node == 'NODE'
99
102
  ```
100
103
  ### `#define_filter_map`
104
+ 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
105
 
102
106
  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
107
 
@@ -115,7 +119,7 @@ class FilterMap
115
119
  define_filter_map :query, :mapped_a, association: :field
116
120
  end
117
121
 
118
- filter_map = FilterMap.new(fytlerz: { query: 'H2O' })
122
+ filter_map = FilterMap.new(fylterz: { query: 'H2O' })
119
123
 
120
124
  filter_map.mapped_a == 'H2O'
121
125
  filter_map.association_field = 'H2O'
@@ -227,6 +231,88 @@ filtered_authors = AuthorFilter.new(filters: filters).results
227
231
 
228
232
  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
233
 
234
+ 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:-
235
+
236
+
237
+ ```ruby
238
+ class AuthorFilter
239
+ include Rokaki::FilterModel
240
+
241
+ filter_map :author, :query,
242
+ like: {
243
+ articles: {
244
+ title: :circumfix,
245
+ or: { # the or is aware of the join and will generate a compound join aware or query
246
+ reviews: {
247
+ title: :circumfix
248
+ }
249
+ }
250
+ },
251
+ }
252
+
253
+ attr_accessor :filters, :model
254
+
255
+ def initialize(filters:)
256
+ @filters = filters
257
+ end
258
+ end
259
+
260
+ filters = { query: "Lao" }
261
+ filtered_authors = AuthorFilter.new(filters: filters).results
262
+ ```
263
+
264
+ ## CAVEATS
265
+ 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:-
266
+
267
+ ### #structurally_incompatible_values_for_or
268
+
269
+ ``` ruby
270
+ module ActiveRecord
271
+ module QueryMethods
272
+ def structurally_incompatible_values_for_or(other)
273
+ Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } +
274
+ (Relation::MULTI_VALUE_METHODS - [:joins, :eager_load, :references, :extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } +
275
+ (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") }
276
+ end
277
+ end
278
+ end
279
+ ```
280
+
281
+ ### A has one relation to a model called Or
282
+ If you happen to have a model/table named 'Or' then you can override the `or:` key syntax by specifying a special `or_key`:-
283
+
284
+ ```ruby
285
+ class AuthorFilter
286
+ include Rokaki::FilterModel
287
+
288
+ or_key :my_or
289
+ filter_map :author, :query,
290
+ like: {
291
+ articles: {
292
+ title: :circumfix,
293
+ my_or: { # the or is aware of the join and will generate a compound join aware or query
294
+ or: { # The Or model has a title field
295
+ title: :circumfix
296
+ }
297
+ }
298
+ },
299
+ }
300
+
301
+ attr_accessor :filters, :model
302
+
303
+ def initialize(filters:)
304
+ @filters = filters
305
+ end
306
+ end
307
+
308
+ filters = { query: "Syntaxes" }
309
+ filtered_authors = AuthorFilter.new(filters: filters).results
310
+ ```
311
+
312
+
313
+ See [this issue](https://github.com/rails/rails/issues/24055) for details.
314
+
315
+
230
316
  #### 3. The porcelain command syntax
231
317
 
232
318
  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`
@@ -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
@@ -27,9 +27,10 @@ module Rokaki
27
27
 
28
28
  def filter_map(model, query_key, options)
29
29
  filter_model(model)
30
- @_query_key = query_key
30
+ @filter_map_query_key = query_key
31
31
 
32
32
  @_filter_db = options[:db] || :postgres
33
+ @_filter_mode = options[:mode] || :and
33
34
  like(options[:like]) if options[:like]
34
35
  ilike(options[:ilike]) if options[:ilike]
35
36
  filters(*options[:match]) if options[:match]
@@ -37,34 +38,57 @@ module Rokaki
37
38
 
38
39
  def filter(model, options)
39
40
  filter_model(model)
41
+ @filter_map_query_key = nil
40
42
 
41
43
  @_filter_db = options[:db] || :postgres
44
+ @_filter_mode = options[:mode] || :and
42
45
  like(options[:like]) if options[:like]
43
46
  ilike(options[:ilike]) if options[:ilike]
44
47
  filters(*options[:match]) if options[:match]
45
48
  end
46
49
 
47
50
  def filters(*filter_keys)
48
- if @_query_key
49
- define_filter_map(@_query_key, *filter_keys)
51
+ if @filter_map_query_key
52
+ define_filter_map(@filter_map_query_key, *filter_keys)
50
53
  else
51
54
  define_filter_keys(*filter_keys)
52
55
  end
53
56
 
54
57
  @_chain_filters ||= []
55
58
  filter_keys.each do |filter_key|
56
-
57
59
  # TODO: does the key need casting to an array here?
58
60
  _chain_filter(filter_key) unless filter_key.is_a? Hash
59
-
60
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
61
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)
62
72
  end
63
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)
64
88
  define_results # writes out all the generated filters
65
89
  end
66
90
 
67
- def _chain_filter(key)
91
+ def _build_basic_filter(key)
68
92
  basic_filter = BasicFilter.new(
69
93
  keys: [key],
70
94
  prefix: filter_key_prefix,
@@ -74,13 +98,17 @@ module Rokaki
74
98
  db: @_filter_db
75
99
  )
76
100
  basic_filter.call
101
+ basic_filter
102
+ end
77
103
 
104
+ def _chain_filter(key)
105
+ basic_filter = _build_basic_filter(key)
78
106
  class_eval basic_filter.filter_method, __FILE__, __LINE__ - 2
79
107
 
80
108
  @_chain_filters << basic_filter.filter_template
81
109
  end
82
110
 
83
- def _chain_nested_filter(filters_object)
111
+ def _build_nested_filter(filters_object)
84
112
  nested_filter = NestedFilter.new(
85
113
  filter_key_object: filters_object,
86
114
  prefix: filter_key_prefix,
@@ -90,18 +118,29 @@ module Rokaki
90
118
  db: @_filter_db
91
119
  )
92
120
  nested_filter.call
121
+ nested_filter
122
+ end
93
123
 
94
- nested_filter.filter_methods.each do |filter_method|
124
+ def _chain_nested_like_filter(filters_object)
125
+ filters_object.filter_methods.each do |filter_method|
95
126
  class_eval filter_method, __FILE__, __LINE__ - 2
96
127
  end
97
128
 
98
- nested_filter.filter_templates.each do |filter_template|
129
+ filters_object.templates.each do |filter_template|
99
130
  @_chain_filters << filter_template
100
131
  end
101
132
  end
102
133
 
103
- def associated_table(association)
104
- @model.reflect_on_association(association).klass.table_name
134
+ def _chain_nested_filter(filters_object)
135
+ nested_filter = _build_nested_filter(filters_object)
136
+
137
+ nested_filter.filter_methods.each do |filter_method|
138
+ class_eval filter_method, __FILE__, __LINE__ - 2
139
+ end
140
+
141
+ nested_filter.filter_templates.each do |filter_template|
142
+ @_chain_filters << filter_template
143
+ end
105
144
  end
106
145
 
107
146
  def filter_model(model_class)
@@ -113,41 +152,16 @@ module Rokaki
113
152
  raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
114
153
  @_like_semantics = (@_like_semantics || {}).merge(args)
115
154
 
116
- key_builder = LikeKeys.new(args)
117
- keys = key_builder.call
118
-
119
- filters(*keys)
155
+ like_keys = LikeKeys.new(args)
156
+ like_filters(like_keys, term_type: :like)
120
157
  end
121
158
 
122
159
  def ilike(args)
123
160
  raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
124
161
  @i_like_semantics = (@i_like_semantics || {}).merge(args)
125
162
 
126
- key_builder = LikeKeys.new(args)
127
- keys = key_builder.call
128
-
129
- filters(*keys)
130
- end
131
-
132
- def deep_chain(keys, value)
133
- if value.is_a? Hash
134
- value.keys.map do |key|
135
- _keys = keys.dup << key
136
- deep_chain(_keys, value[key])
137
- end
138
- end
139
-
140
- if value.is_a? Array
141
- value.each do |av|
142
- _keys = keys.dup << av
143
- _build_deep_chain(_keys)
144
- end
145
- end
146
-
147
- if value.is_a? Symbol
148
- _keys = keys.dup << value
149
- _build_deep_chain(_keys)
150
- end
163
+ like_keys = LikeKeys.new(args)
164
+ like_filters(like_keys, term_type: :ilike)
151
165
  end
152
166
 
153
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
@@ -4,7 +4,7 @@ require 'active_support/inflector'
4
4
  module Rokaki
5
5
  module FilterModel
6
6
  class NestedFilter
7
- def initialize(filter_key_object:, prefix:, infix:, like_semantics:, i_like_semantics:, db:)
7
+ def initialize(filter_key_object:, prefix:, infix:, like_semantics:, i_like_semantics:, db:, mode: :and)
8
8
  @filter_key_object = filter_key_object
9
9
  @prefix = prefix
10
10
  @infix = infix
@@ -13,8 +13,9 @@ module Rokaki
13
13
  @filter_methods = []
14
14
  @filter_templates = []
15
15
  @db = db
16
+ @mode = mode
16
17
  end
17
- attr_reader :filter_key_object, :prefix, :infix, :like_semantics, :i_like_semantics, :db
18
+ attr_reader :filter_key_object, :prefix, :infix, :like_semantics, :i_like_semantics, :db, :mode
18
19
  attr_accessor :filter_methods, :filter_templates
19
20
 
20
21
  def call # _chain_nested_filter
@@ -56,7 +57,7 @@ module Rokaki
56
57
  end
57
58
 
58
59
  def find_i_like_key(keys)
59
- return nil unless like_semantics && i_like_semantics.keys.any?
60
+ return nil unless i_like_semantics && i_like_semantics.keys.any?
60
61
  current_like_key = i_like_semantics
61
62
  keys.each do |key|
62
63
  current_like_key = current_like_key[key]
@@ -65,7 +66,7 @@ module Rokaki
65
66
  end
66
67
 
67
68
  def _build_deep_chain(keys)
68
- name = '' # prefix.to_s
69
+ name = ''
69
70
  count = keys.size - 1
70
71
 
71
72
  joins_before = []
@@ -74,13 +75,13 @@ module Rokaki
74
75
  where_before = []
75
76
  where_after = []
76
77
  out = ''
77
- mode = nil
78
+ search_mode = nil
78
79
  type = nil
79
80
  leaf = nil
80
81
 
81
- if mode = find_like_key(keys)
82
+ if search_mode = find_like_key(keys)
82
83
  type = 'LIKE'
83
- elsif mode = find_i_like_key(keys)
84
+ elsif search_mode = find_i_like_key(keys)
84
85
  type = 'ILIKE'
85
86
  end
86
87
  leaf = keys.pop
@@ -113,12 +114,12 @@ module Rokaki
113
114
  joins = joins.join
114
115
  where = where.join
115
116
 
116
- if mode
117
+ if search_mode
117
118
  query = build_like_query(
118
119
  type: type,
119
120
  query: '',
120
121
  filter: "#{prefix}#{name}",
121
- mode: mode,
122
+ search_mode: search_mode,
122
123
  key: keys.last.to_s.pluralize,
123
124
  leaf: leaf
124
125
  )
@@ -135,15 +136,15 @@ module Rokaki
135
136
  end
136
137
  end
137
138
 
138
- def build_like_query(type:, query:, filter:, mode:, key:, leaf:)
139
+ def build_like_query(type:, query:, filter:, search_mode:, key:, leaf:)
139
140
  if db == :postgres
140
141
  query = "where(\"#{key}.#{leaf} #{type} ANY (ARRAY[?])\", "
141
- query += "prepare_terms(#{filter}, :#{mode}))"
142
+ query += "prepare_terms(#{filter}, :#{search_mode}))"
142
143
  else
143
144
  query = "where(\"#{key}.#{leaf} #{type} :query\", "
144
- query += "query: \"%\#{#{filter}}%\")" if mode == :circumfix
145
- query += "query: \"%\#{#{filter}}\")" if mode == :prefix
146
- 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
147
148
  end
148
149
 
149
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,18 +15,22 @@ 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
 
30
+ def define_query_key(key = nil)
31
+ @filter_map_query_key = key
32
+ end
33
+
26
34
  def filter_key_prefix(prefix = nil)
27
35
  @filter_key_prefix ||= prefix
28
36
  end
@@ -31,11 +39,16 @@ module Rokaki
31
39
  @filter_key_infix ||= infix
32
40
  end
33
41
 
42
+ def or_key(or_key = :or)
43
+ @or_key ||= or_key
44
+ end
45
+
34
46
  def filterable_object_name(name = 'filters')
35
47
  @filterable_object_name ||= name
36
48
  end
37
49
 
38
50
  def _build_filter(keys)
51
+ keys.delete(or_key)
39
52
  name = @filter_key_prefix.to_s
40
53
  count = keys.size - 1
41
54
 
@@ -48,6 +61,7 @@ module Rokaki
48
61
  end
49
62
 
50
63
  def _map_filters(query_field, keys)
64
+ keys.delete(or_key)
51
65
  name = @filter_key_prefix.to_s
52
66
  count = keys.size - 1
53
67
 
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.3.1"
3
3
  end
@@ -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
4
+ version: 0.8.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Martin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-21 00:00:00.000000000 Z
11
+ date: 2020-06-28 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
  - - ">="
@@ -200,9 +214,12 @@ files:
200
214
  - lib/rokaki.rb
201
215
  - lib/rokaki/filter_model.rb
202
216
  - lib/rokaki/filter_model/basic_filter.rb
217
+ - lib/rokaki/filter_model/deep_assign_struct.rb
203
218
  - lib/rokaki/filter_model/filter_chain.rb
219
+ - lib/rokaki/filter_model/join_map.rb
204
220
  - lib/rokaki/filter_model/like_keys.rb
205
221
  - lib/rokaki/filter_model/nested_filter.rb
222
+ - lib/rokaki/filter_model/nested_like_filters.rb
206
223
  - lib/rokaki/filterable.rb
207
224
  - lib/rokaki/version.rb
208
225
  - rokaki.gemspec