active_record_seek 0.0.1

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.
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