rokaki 0.8.0 → 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +36 -0
- data/lib/rokaki.rb +2 -0
- data/lib/rokaki/filter_model.rb +53 -40
- data/lib/rokaki/filter_model/basic_filter.rb +3 -2
- data/lib/rokaki/filter_model/deep_assign_struct.rb +57 -0
- data/lib/rokaki/filter_model/join_map.rb +104 -0
- data/lib/rokaki/filter_model/like_keys.rb +24 -22
- data/lib/rokaki/filter_model/nested_filter.rb +14 -13
- data/lib/rokaki/filter_model/nested_like_filters.rb +241 -0
- data/lib/rokaki/filterable.rb +10 -0
- data/lib/rokaki/version.rb +1 -1
- data/rokaki.gemspec +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53402d2c7b2fca689234d8bed13602eef1bac7a161b09fe15da517da8939e5ac
|
4
|
+
data.tar.gz: 2a903781f7d5f2e4b90c419def1dc5a2b8289798a708a66e6be91604de02de86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cd1957fe50f484e465a7c592a6e770e6a0e46975479ea4863deab66434031936d477b852ae8b72095943bc0db47d1379cd089b280485f5d4a7aa90d50cc6a12
|
7
|
+
data.tar.gz: 9b966c7cafd777655fcb6f20b70b3c331d2f8175c8bf22c49ef4295188f25ae15a7b8581738f4fbac5eb5e605b129088936c8588097015d366080b8866687891
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rokaki (0.8.
|
4
|
+
rokaki (0.8.3)
|
5
5
|
activesupport
|
6
6
|
|
7
7
|
GEM
|
@@ -59,7 +59,7 @@ GEM
|
|
59
59
|
pry-byebug (3.9.0)
|
60
60
|
byebug (~> 11.0)
|
61
61
|
pry (~> 0.13.0)
|
62
|
-
rake (
|
62
|
+
rake (13.0.1)
|
63
63
|
rb-fsevent (0.10.4)
|
64
64
|
rb-inotify (0.10.1)
|
65
65
|
ffi (~> 1.0)
|
@@ -96,7 +96,7 @@ DEPENDENCIES
|
|
96
96
|
pg
|
97
97
|
pry
|
98
98
|
pry-byebug
|
99
|
-
rake (~>
|
99
|
+
rake (~> 13.0)
|
100
100
|
rokaki!
|
101
101
|
rspec (~> 3.0)
|
102
102
|
sqlite3
|
data/README.md
CHANGED
@@ -8,6 +8,12 @@ There are two modes of use `Filterable` and `FilterModel` that can be activated
|
|
8
8
|
|
9
9
|
Add this line to your application's Gemfile:
|
10
10
|
|
11
|
+
You can install from Rubygems:
|
12
|
+
```
|
13
|
+
gem 'rokaki'
|
14
|
+
```
|
15
|
+
Or from github
|
16
|
+
|
11
17
|
```ruby
|
12
18
|
gem 'rokaki', git: 'https://github.com/tevio/rokaki.git'
|
13
19
|
```
|
@@ -221,6 +227,36 @@ filtered_authors = AuthorFilter.new(filters: filters).results
|
|
221
227
|
|
222
228
|
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.
|
223
229
|
|
230
|
+
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:-
|
231
|
+
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
class AuthorFilter
|
235
|
+
include Rokaki::FilterModel
|
236
|
+
|
237
|
+
filter_map :author, :query,
|
238
|
+
like: {
|
239
|
+
articles: {
|
240
|
+
title: :circumfix,
|
241
|
+
or: { # the or is aware of the join and will generate a compound join aware or query
|
242
|
+
reviews: {
|
243
|
+
title: :circumfix
|
244
|
+
}
|
245
|
+
}
|
246
|
+
},
|
247
|
+
}
|
248
|
+
|
249
|
+
attr_accessor :filters, :model
|
250
|
+
|
251
|
+
def initialize(filters:)
|
252
|
+
@filters = filters
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
filters = { query: "Lao" }
|
257
|
+
filtered_authors = AuthorFilter.new(filters: filters).results
|
258
|
+
```
|
259
|
+
|
224
260
|
#### 3. The porcelain command syntax
|
225
261
|
|
226
262
|
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,9 +3,11 @@
|
|
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
|
data/lib/rokaki/filter_model.rb
CHANGED
@@ -27,9 +27,10 @@ module Rokaki
|
|
27
27
|
|
28
28
|
def filter_map(model, query_key, options)
|
29
29
|
filter_model(model)
|
30
|
-
@
|
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,56 @@ 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 @
|
49
|
-
define_filter_map(@
|
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
|
+
)
|
84
|
+
nested_like_filter.call
|
85
|
+
|
86
|
+
_chain_nested_like_filter(nested_like_filter)
|
64
87
|
define_results # writes out all the generated filters
|
65
88
|
end
|
66
89
|
|
67
|
-
def
|
90
|
+
def _build_basic_filter(key)
|
68
91
|
basic_filter = BasicFilter.new(
|
69
92
|
keys: [key],
|
70
93
|
prefix: filter_key_prefix,
|
@@ -74,13 +97,17 @@ module Rokaki
|
|
74
97
|
db: @_filter_db
|
75
98
|
)
|
76
99
|
basic_filter.call
|
100
|
+
basic_filter
|
101
|
+
end
|
77
102
|
|
103
|
+
def _chain_filter(key)
|
104
|
+
basic_filter = _build_basic_filter(key)
|
78
105
|
class_eval basic_filter.filter_method, __FILE__, __LINE__ - 2
|
79
106
|
|
80
107
|
@_chain_filters << basic_filter.filter_template
|
81
108
|
end
|
82
109
|
|
83
|
-
def
|
110
|
+
def _build_nested_filter(filters_object)
|
84
111
|
nested_filter = NestedFilter.new(
|
85
112
|
filter_key_object: filters_object,
|
86
113
|
prefix: filter_key_prefix,
|
@@ -90,18 +117,29 @@ module Rokaki
|
|
90
117
|
db: @_filter_db
|
91
118
|
)
|
92
119
|
nested_filter.call
|
120
|
+
nested_filter
|
121
|
+
end
|
93
122
|
|
94
|
-
|
123
|
+
def _chain_nested_like_filter(filters_object)
|
124
|
+
filters_object.filter_methods.each do |filter_method|
|
95
125
|
class_eval filter_method, __FILE__, __LINE__ - 2
|
96
126
|
end
|
97
127
|
|
98
|
-
|
128
|
+
filters_object.templates.each do |filter_template|
|
99
129
|
@_chain_filters << filter_template
|
100
130
|
end
|
101
131
|
end
|
102
132
|
|
103
|
-
def
|
104
|
-
|
133
|
+
def _chain_nested_filter(filters_object)
|
134
|
+
nested_filter = _build_nested_filter(filters_object)
|
135
|
+
|
136
|
+
nested_filter.filter_methods.each do |filter_method|
|
137
|
+
class_eval filter_method, __FILE__, __LINE__ - 2
|
138
|
+
end
|
139
|
+
|
140
|
+
nested_filter.filter_templates.each do |filter_template|
|
141
|
+
@_chain_filters << filter_template
|
142
|
+
end
|
105
143
|
end
|
106
144
|
|
107
145
|
def filter_model(model_class)
|
@@ -113,41 +151,16 @@ module Rokaki
|
|
113
151
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
114
152
|
@_like_semantics = (@_like_semantics || {}).merge(args)
|
115
153
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
filters(*keys)
|
154
|
+
like_keys = LikeKeys.new(args)
|
155
|
+
like_filters(like_keys, term_type: :like)
|
120
156
|
end
|
121
157
|
|
122
158
|
def ilike(args)
|
123
159
|
raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
|
124
160
|
@i_like_semantics = (@i_like_semantics || {}).merge(args)
|
125
161
|
|
126
|
-
|
127
|
-
|
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
|
162
|
+
like_keys = LikeKeys.new(args)
|
163
|
+
like_filters(like_keys, term_type: :ilike)
|
151
164
|
end
|
152
165
|
|
153
166
|
# 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,104 @@
|
|
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 key_path.is_a?(Symbol)
|
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 key_path.is_a?(Array)
|
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 parent_result.is_a?(Symbol) && parent_result != key
|
46
|
+
result[previous_key] = [parent_result, key]
|
47
|
+
elsif parent_result.is_a?(Array)
|
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
|
+
if res.is_a? Symbol
|
67
|
+
result[previous_key_path.first] = { previous_key => key }
|
68
|
+
elsif res.is_a?(Hash)
|
69
|
+
end
|
70
|
+
elsif previous_path_length > 1
|
71
|
+
res = result.dig(*previous_key_path)
|
72
|
+
if res.is_a? Symbol
|
73
|
+
base = previous_key_path.pop
|
74
|
+
result.dig(*previous_key_path)[base] = { previous_key => key }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
else
|
80
|
+
end
|
81
|
+
|
82
|
+
previous_key = key
|
83
|
+
current_key_path << key
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
|
@@ -9,43 +9,45 @@ module Rokaki
|
|
9
9
|
class LikeKeys
|
10
10
|
def initialize(args)
|
11
11
|
@args = args
|
12
|
-
@
|
12
|
+
@keys = []
|
13
|
+
@key_paths = []
|
13
14
|
end
|
14
15
|
|
15
|
-
attr_reader :args, :
|
16
|
+
attr_reader :args, :keys, :key_paths
|
16
17
|
|
17
18
|
def call
|
18
19
|
args.keys.each do |key|
|
19
|
-
|
20
|
+
map_keys(key: key, value: args[key])
|
20
21
|
end
|
21
|
-
|
22
|
+
keys
|
22
23
|
end
|
23
24
|
|
24
25
|
private
|
25
26
|
|
26
|
-
def map_keys(value
|
27
|
-
key_result = {}
|
27
|
+
def map_keys(key:, value:, key_path: [])
|
28
28
|
|
29
|
-
if value.is_a?
|
30
|
-
|
31
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
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]
|
@@ -74,13 +75,13 @@ module Rokaki
|
|
74
75
|
where_before = []
|
75
76
|
where_after = []
|
76
77
|
out = ''
|
77
|
-
|
78
|
+
search_mode = nil
|
78
79
|
type = nil
|
79
80
|
leaf = nil
|
80
81
|
|
81
|
-
if
|
82
|
+
if search_mode = find_like_key(keys)
|
82
83
|
type = 'LIKE'
|
83
|
-
elsif
|
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
|
117
|
+
if search_mode
|
117
118
|
query = build_like_query(
|
118
119
|
type: type,
|
119
120
|
query: '',
|
120
121
|
filter: "#{prefix}#{name}",
|
121
|
-
|
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:,
|
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}, :#{
|
142
|
+
query += "prepare_terms(#{filter}, :#{search_mode}))"
|
142
143
|
else
|
143
144
|
query = "where(\"#{key}.#{leaf} #{type} :query\", "
|
144
|
-
query += "query: \"%\#{#{filter}}%\")" if
|
145
|
-
query += "query: \"%\#{#{filter}}\")" if
|
146
|
-
query += "query: \"\#{#{filter}}%\")" if
|
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_filter(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_filter(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_filter(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
|
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
|
+
|
data/lib/rokaki/filterable.rb
CHANGED
@@ -23,6 +23,10 @@ module Rokaki
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
def define_query_key(key = nil)
|
27
|
+
@filter_map_query_key = key
|
28
|
+
end
|
29
|
+
|
26
30
|
def filter_key_prefix(prefix = nil)
|
27
31
|
@filter_key_prefix ||= prefix
|
28
32
|
end
|
@@ -31,11 +35,16 @@ module Rokaki
|
|
31
35
|
@filter_key_infix ||= infix
|
32
36
|
end
|
33
37
|
|
38
|
+
def or_key(or_key = :or)
|
39
|
+
@or_key ||= or_key
|
40
|
+
end
|
41
|
+
|
34
42
|
def filterable_object_name(name = 'filters')
|
35
43
|
@filterable_object_name ||= name
|
36
44
|
end
|
37
45
|
|
38
46
|
def _build_filter(keys)
|
47
|
+
keys.delete(or_key)
|
39
48
|
name = @filter_key_prefix.to_s
|
40
49
|
count = keys.size - 1
|
41
50
|
|
@@ -48,6 +57,7 @@ module Rokaki
|
|
48
57
|
end
|
49
58
|
|
50
59
|
def _map_filters(query_field, keys)
|
60
|
+
keys.delete(or_key)
|
51
61
|
name = @filter_key_prefix.to_s
|
52
62
|
count = keys.size - 1
|
53
63
|
|
data/lib/rokaki/version.rb
CHANGED
data/rokaki.gemspec
CHANGED
@@ -40,7 +40,7 @@ Gem::Specification.new do |spec|
|
|
40
40
|
spec.add_development_dependency 'pg'
|
41
41
|
spec.add_development_dependency 'pry'
|
42
42
|
spec.add_development_dependency 'pry-byebug'
|
43
|
-
spec.add_development_dependency 'rake', '~>
|
43
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
44
44
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
45
45
|
spec.add_development_dependency 'sqlite3'
|
46
46
|
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.
|
4
|
+
version: 0.8.3
|
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-
|
11
|
+
date: 2020-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
145
|
+
version: '13.0'
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: '
|
152
|
+
version: '13.0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: rspec
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,9 +200,12 @@ files:
|
|
200
200
|
- lib/rokaki.rb
|
201
201
|
- lib/rokaki/filter_model.rb
|
202
202
|
- lib/rokaki/filter_model/basic_filter.rb
|
203
|
+
- lib/rokaki/filter_model/deep_assign_struct.rb
|
203
204
|
- lib/rokaki/filter_model/filter_chain.rb
|
205
|
+
- lib/rokaki/filter_model/join_map.rb
|
204
206
|
- lib/rokaki/filter_model/like_keys.rb
|
205
207
|
- lib/rokaki/filter_model/nested_filter.rb
|
208
|
+
- lib/rokaki/filter_model/nested_like_filters.rb
|
206
209
|
- lib/rokaki/filterable.rb
|
207
210
|
- lib/rokaki/version.rb
|
208
211
|
- rokaki.gemspec
|