active_record_seek 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +45 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +47 -0
  10. data/Rakefile +21 -0
  11. data/active_record_seek.gemspec +31 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/gemfiles/mysql.gemfile +5 -0
  15. data/gemfiles/mysql.gemfile.lock +47 -0
  16. data/gemfiles/postgresql.gemfile +5 -0
  17. data/gemfiles/postgresql.gemfile.lock +47 -0
  18. data/gemfiles/sqlite.gemfile +7 -0
  19. data/gemfiles/sqlite.gemfile.lock +47 -0
  20. data/lib/active_record_seek.rb +29 -0
  21. data/lib/active_record_seek/component.rb +57 -0
  22. data/lib/active_record_seek/concerns/active_record_concern.rb +20 -0
  23. data/lib/active_record_seek/concerns/instance_variable_concern.rb +40 -0
  24. data/lib/active_record_seek/middleware.rb +24 -0
  25. data/lib/active_record_seek/operators/base_operator.rb +65 -0
  26. data/lib/active_record_seek/operators/ci_base_operator.rb +66 -0
  27. data/lib/active_record_seek/operators/ci_matches_base_operator.rb +38 -0
  28. data/lib/active_record_seek/operators/ci_regexp_base_operator.rb +46 -0
  29. data/lib/active_record_seek/operators/matches_base_operator.rb +32 -0
  30. data/lib/active_record_seek/operators/regexp_base_operator.rb +46 -0
  31. data/lib/active_record_seek/query.rb +68 -0
  32. data/lib/active_record_seek/scopes/association_scope.rb +98 -0
  33. data/lib/active_record_seek/scopes/base_scope.rb +10 -0
  34. data/lib/active_record_seek/scopes/seek_or_scope.rb +53 -0
  35. data/lib/active_record_seek/scopes/seek_scope.rb +54 -0
  36. data/lib/active_record_seek/version.rb +3 -0
  37. 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