active_record_seek 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +21 -0
- data/active_record_seek.gemspec +31 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/mysql.gemfile +5 -0
- data/gemfiles/mysql.gemfile.lock +47 -0
- data/gemfiles/postgresql.gemfile +5 -0
- data/gemfiles/postgresql.gemfile.lock +47 -0
- data/gemfiles/sqlite.gemfile +7 -0
- data/gemfiles/sqlite.gemfile.lock +47 -0
- data/lib/active_record_seek.rb +29 -0
- data/lib/active_record_seek/component.rb +57 -0
- data/lib/active_record_seek/concerns/active_record_concern.rb +20 -0
- data/lib/active_record_seek/concerns/instance_variable_concern.rb +40 -0
- data/lib/active_record_seek/middleware.rb +24 -0
- data/lib/active_record_seek/operators/base_operator.rb +65 -0
- data/lib/active_record_seek/operators/ci_base_operator.rb +66 -0
- data/lib/active_record_seek/operators/ci_matches_base_operator.rb +38 -0
- data/lib/active_record_seek/operators/ci_regexp_base_operator.rb +46 -0
- data/lib/active_record_seek/operators/matches_base_operator.rb +32 -0
- data/lib/active_record_seek/operators/regexp_base_operator.rb +46 -0
- data/lib/active_record_seek/query.rb +68 -0
- data/lib/active_record_seek/scopes/association_scope.rb +98 -0
- data/lib/active_record_seek/scopes/base_scope.rb +10 -0
- data/lib/active_record_seek/scopes/seek_or_scope.rb +53 -0
- data/lib/active_record_seek/scopes/seek_scope.rb +54 -0
- data/lib/active_record_seek/version.rb +3 -0
- metadata +149 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
require "byebug"
|
2
|
+
require "active_support"
|
3
|
+
require "active_support/core_ext"
|
4
|
+
require "active_record_seek/version"
|
5
|
+
# concerns
|
6
|
+
require "active_record_seek/concerns/instance_variable_concern"
|
7
|
+
# core
|
8
|
+
require "active_record_seek/query"
|
9
|
+
require "active_record_seek/component"
|
10
|
+
require "active_record_seek/middleware"
|
11
|
+
# operators
|
12
|
+
require "active_record_seek/operators/base_operator"
|
13
|
+
require "active_record_seek/operators/matches_base_operator"
|
14
|
+
require "active_record_seek/operators/regexp_base_operator"
|
15
|
+
require "active_record_seek/operators/ci_base_operator"
|
16
|
+
require "active_record_seek/operators/ci_matches_base_operator"
|
17
|
+
require "active_record_seek/operators/ci_regexp_base_operator"
|
18
|
+
# scopes
|
19
|
+
require "active_record_seek/scopes/base_scope"
|
20
|
+
require "active_record_seek/scopes/association_scope"
|
21
|
+
require "active_record_seek/scopes/seek_or_scope"
|
22
|
+
require "active_record_seek/scopes/seek_scope"
|
23
|
+
# callbacks
|
24
|
+
require "active_record_seek/concerns/active_record_concern"
|
25
|
+
|
26
|
+
ActiveSupport.on_load(:active_record) do
|
27
|
+
# adds builder methods used to initialize seek scopes on columns of a model
|
28
|
+
include ActiveRecordSeek::Concerns::ActiveRecordConcern
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
class Component
|
3
|
+
|
4
|
+
include Concerns::InstanceVariableConcern
|
5
|
+
|
6
|
+
attr_accessor(*%w[
|
7
|
+
base_query
|
8
|
+
value
|
9
|
+
operator
|
10
|
+
column
|
11
|
+
namespace
|
12
|
+
original_key
|
13
|
+
])
|
14
|
+
attr_reader(*%w[ association ])
|
15
|
+
delegate(*%w[ active_record_query seek_query ], to: :base_query)
|
16
|
+
delegate(*%w[ table_name adapter_name ], to: :seek_query)
|
17
|
+
|
18
|
+
|
19
|
+
def key=(new_key)
|
20
|
+
self.original_key = new_key.to_s
|
21
|
+
parts = original_key.split(".").select(&:present?)
|
22
|
+
self.operator = parts.pop
|
23
|
+
self.column = parts.pop
|
24
|
+
self.association = parts.pop || "self"
|
25
|
+
self.namespace = parts.pop || "default"
|
26
|
+
key
|
27
|
+
end
|
28
|
+
|
29
|
+
def key
|
30
|
+
"#{namespace}.#{association}.#{column}.#{operator}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def association=(new_association)
|
34
|
+
@association = new_association == "self" ? table_name : new_association
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_base_query_component?
|
38
|
+
association == table_name
|
39
|
+
end
|
40
|
+
|
41
|
+
def operator_class
|
42
|
+
"::ActiveRecordSeek::Operators::#{operator.camelcase}Operator".constantize
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply(query)
|
46
|
+
Middleware.middleware.each do |middleware|
|
47
|
+
action = middleware.call(self)
|
48
|
+
case action
|
49
|
+
when :skip then return query
|
50
|
+
else # no action
|
51
|
+
end
|
52
|
+
end
|
53
|
+
operator_class.new(component: self).apply(query)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Concerns
|
3
|
+
module ActiveRecordConcern
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include ActiveRecordSeek::Scopes::SeekScope::ActiveRecordScopeConcern
|
9
|
+
include ActiveRecordSeek::Scopes::SeekOrScope::ActiveRecordScopeConcern
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def to_seek_query
|
14
|
+
::ActiveRecordSeek::Query.new(active_record_query: all)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Concerns
|
3
|
+
module InstanceVariableConcern
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def initialize(variables = {})
|
8
|
+
set(variables)
|
9
|
+
after_initialize
|
10
|
+
end
|
11
|
+
|
12
|
+
def after_initialize
|
13
|
+
# noop
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(params = {})
|
17
|
+
params.each { |key, value| send("#{key}=", value) }
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def instance_variable_reset(variable, &block)
|
22
|
+
if instance_variable_defined?(variable)
|
23
|
+
remove_instance_variable(variable)
|
24
|
+
true
|
25
|
+
else
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def instance_variable_yield(variable)
|
31
|
+
if instance_variable_defined?(variable)
|
32
|
+
value = instance_variable_get(variable)
|
33
|
+
yield(value)
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
class Middleware
|
3
|
+
|
4
|
+
attr_accessor(*%w[ name middleware_block ])
|
5
|
+
|
6
|
+
def initialize(name:, &middleware_block)
|
7
|
+
raise(ArgumentError, "#{self.class} expects a block") if !middleware_block
|
8
|
+
self.name = name.to_s
|
9
|
+
self.middleware_block = middleware_block
|
10
|
+
self.class.middleware.push(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(*params, &block)
|
14
|
+
middleware_block.call(*params, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def middleware
|
19
|
+
@middleware ||= []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class BaseOperator
|
4
|
+
|
5
|
+
include Concerns::InstanceVariableConcern
|
6
|
+
|
7
|
+
attr_accessor(*%w[ component query ])
|
8
|
+
attr_writer(*%w[ arel_table arel_column arel_value ])
|
9
|
+
|
10
|
+
def arel_table
|
11
|
+
query.arel_table
|
12
|
+
end
|
13
|
+
|
14
|
+
def arel_column
|
15
|
+
arel_table[component.column]
|
16
|
+
end
|
17
|
+
|
18
|
+
def arel_value
|
19
|
+
component.value
|
20
|
+
end
|
21
|
+
|
22
|
+
def arel_operation
|
23
|
+
arel_column.send(component.operator, arel_value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply(query)
|
27
|
+
set(query: query)
|
28
|
+
query.where(arel_operation)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
%w[
|
34
|
+
between
|
35
|
+
eq
|
36
|
+
eq_all
|
37
|
+
eq_any
|
38
|
+
gt
|
39
|
+
gt_all
|
40
|
+
gt_any
|
41
|
+
gteq
|
42
|
+
gteq_all
|
43
|
+
gteq_any
|
44
|
+
in
|
45
|
+
in_all
|
46
|
+
in_any
|
47
|
+
lt
|
48
|
+
lt_all
|
49
|
+
lt_any
|
50
|
+
lteq
|
51
|
+
lteq_all
|
52
|
+
lteq_any
|
53
|
+
not_between
|
54
|
+
not_eq
|
55
|
+
not_eq_all
|
56
|
+
not_eq_any
|
57
|
+
not_in
|
58
|
+
not_in_all
|
59
|
+
not_in_any
|
60
|
+
].each do |operator|
|
61
|
+
const_set("#{operator.camelcase}Operator", Class.new(BaseOperator))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class CiBaseOperator < BaseOperator
|
4
|
+
|
5
|
+
def ci_arel_column
|
6
|
+
arel_column.lower
|
7
|
+
end
|
8
|
+
|
9
|
+
def ci_arel_value(value = arel_value)
|
10
|
+
if value.is_a?(Array)
|
11
|
+
value.map { |v| ci_arel_value(v) }
|
12
|
+
elsif value.is_a?(String)
|
13
|
+
arel_table.lower(value)
|
14
|
+
else
|
15
|
+
value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def arel_operation
|
20
|
+
ci_arel_column.send(component.operator, ci_arel_value)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
%w[
|
26
|
+
eq
|
27
|
+
eq_all
|
28
|
+
eq_any
|
29
|
+
gt
|
30
|
+
gt_all
|
31
|
+
gt_any
|
32
|
+
gteq
|
33
|
+
gteq_all
|
34
|
+
gteq_any
|
35
|
+
in
|
36
|
+
in_all
|
37
|
+
in_any
|
38
|
+
lt
|
39
|
+
lt_all
|
40
|
+
lt_any
|
41
|
+
lteq
|
42
|
+
lteq_all
|
43
|
+
lteq_any
|
44
|
+
not_eq
|
45
|
+
not_eq_all
|
46
|
+
not_eq_any
|
47
|
+
not_in
|
48
|
+
not_in_all
|
49
|
+
not_in_any
|
50
|
+
].to_h do |operator|
|
51
|
+
[
|
52
|
+
operator.gsub(/\A(not_)?/, '\1ci_'),
|
53
|
+
operator,
|
54
|
+
]
|
55
|
+
end.each do |seek_operator, arel_operator|
|
56
|
+
operator_class = Class.new(CiBaseOperator) do
|
57
|
+
define_method(:arel_operation) do
|
58
|
+
ci_arel_column.send(arel_operator, ci_arel_value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
const_set("#{seek_operator.to_s.camelcase}Operator", operator_class)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class CiMatchesBaseOperator < CiBaseOperator
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
{
|
8
|
+
ci_like: :matches,
|
9
|
+
ci_like_all: :matches_all,
|
10
|
+
ci_like_any: :matches_any,
|
11
|
+
not_ci_like: :does_not_match,
|
12
|
+
not_ci_like_all: :does_not_match_all,
|
13
|
+
not_ci_like_any: :does_not_match_any,
|
14
|
+
}.each do |seek_operator, arel_operator|
|
15
|
+
operator_class = Class.new(CiMatchesBaseOperator) do
|
16
|
+
define_method(:arel_operation) do
|
17
|
+
operation =
|
18
|
+
# manually LOWER for adapters without ILIKE support
|
19
|
+
if component.adapter_name.in?(%w[ SQLite Mysql2 ])
|
20
|
+
ci_arel_column.send(arel_operator, ci_arel_value)
|
21
|
+
else
|
22
|
+
arel_column.send(arel_operator, arel_value)
|
23
|
+
end
|
24
|
+
# switch to ILIKE if supported
|
25
|
+
operation.each do |operation_part|
|
26
|
+
if operation_part.class.in?([Arel::Nodes::Matches, Arel::Nodes::DoesNotMatch])
|
27
|
+
operation_part.case_sensitive = false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
operation
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
const_set("#{seek_operator.to_s.camelcase}Operator", operator_class)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class CiRegexpBaseOperator < BaseOperator
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
{
|
8
|
+
ci_regexp: :matches_regexp,
|
9
|
+
ci_regexp_all: :matches_regexp,
|
10
|
+
ci_regexp_any: :matches_regexp,
|
11
|
+
not_ci_regexp: :does_not_match_regexp,
|
12
|
+
not_ci_regexp_all: :does_not_match_regexp,
|
13
|
+
not_ci_regexp_any: :does_not_match_regexp,
|
14
|
+
}.each do |seek_operator, arel_operator|
|
15
|
+
operator_class = Class.new(RegexpBaseOperator) do
|
16
|
+
define_method(:arel_operation) do
|
17
|
+
operation = nil
|
18
|
+
# must manually define _all and _any operations because arel only has matches_regexp
|
19
|
+
if seek_operator =~ /_all\z/
|
20
|
+
arel_value.each do |and_arel_value|
|
21
|
+
and_operation = arel_column.send(arel_operator, and_arel_value)
|
22
|
+
operation = operation ? operation.and(and_operation) : and_operation
|
23
|
+
end
|
24
|
+
elsif seek_operator =~ /_any\z/
|
25
|
+
arel_value.each do |or_arel_value|
|
26
|
+
or_operation = arel_column.send(arel_operator, or_arel_value)
|
27
|
+
operation = operation ? operation.or(or_operation) : or_operation
|
28
|
+
end
|
29
|
+
else
|
30
|
+
operation = arel_column.send(arel_operator, arel_value)
|
31
|
+
end
|
32
|
+
# switch to ~ operator
|
33
|
+
operation.each do |operation_part|
|
34
|
+
if operation_part.class.in?([Arel::Nodes::Regexp, Arel::Nodes::NotRegexp])
|
35
|
+
operation_part.case_sensitive = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
operation
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
const_set("#{seek_operator.to_s.camelcase}Operator", operator_class)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class MatchesBaseOperator < BaseOperator
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
{
|
8
|
+
like: :matches,
|
9
|
+
like_all: :matches_all,
|
10
|
+
like_any: :matches_any,
|
11
|
+
not_like: :does_not_match,
|
12
|
+
not_like_all: :does_not_match_all,
|
13
|
+
not_like_any: :does_not_match_any,
|
14
|
+
}.each do |seek_operator, arel_operator|
|
15
|
+
operator_class = Class.new(MatchesBaseOperator) do
|
16
|
+
define_method(:arel_operation) do
|
17
|
+
operation = arel_column.send(arel_operator, arel_value)
|
18
|
+
# switch to LIKE operator
|
19
|
+
operation.each do |operation_part|
|
20
|
+
if operation_part.class.in?([Arel::Nodes::Matches, Arel::Nodes::DoesNotMatch])
|
21
|
+
operation_part.case_sensitive = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
operation
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
const_set("#{seek_operator.to_s.camelcase}Operator", operator_class)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveRecordSeek
|
2
|
+
module Operators
|
3
|
+
class RegexpBaseOperator < BaseOperator
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
{
|
8
|
+
regexp: :matches_regexp,
|
9
|
+
regexp_all: :matches_regexp,
|
10
|
+
regexp_any: :matches_regexp,
|
11
|
+
not_regexp: :does_not_match_regexp,
|
12
|
+
not_regexp_all: :does_not_match_regexp,
|
13
|
+
not_regexp_any: :does_not_match_regexp,
|
14
|
+
}.each do |seek_operator, arel_operator|
|
15
|
+
operator_class = Class.new(RegexpBaseOperator) do
|
16
|
+
define_method(:arel_operation) do
|
17
|
+
operation = nil
|
18
|
+
# must manually define _all and _any operations because arel only has matches_regexp
|
19
|
+
if seek_operator =~ /_all\z/
|
20
|
+
arel_value.each do |and_arel_value|
|
21
|
+
and_operation = arel_column.send(arel_operator, and_arel_value)
|
22
|
+
operation = operation ? operation.and(and_operation) : and_operation
|
23
|
+
end
|
24
|
+
elsif seek_operator =~ /_any\z/
|
25
|
+
arel_value.each do |or_arel_value|
|
26
|
+
or_operation = arel_column.send(arel_operator, or_arel_value)
|
27
|
+
operation = operation ? operation.or(or_operation) : or_operation
|
28
|
+
end
|
29
|
+
else
|
30
|
+
operation = arel_column.send(arel_operator, arel_value)
|
31
|
+
end
|
32
|
+
# switch to ~ operator
|
33
|
+
operation.each do |operation_part|
|
34
|
+
if operation_part.class.in?([Arel::Nodes::Regexp, Arel::Nodes::NotRegexp])
|
35
|
+
operation_part.case_sensitive = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
operation
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
const_set("#{seek_operator.to_s.camelcase}Operator", operator_class)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|