rokaki 0.8.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|