trailblazer-finder 0.1.1 → 0.1.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +160 -11
  3. data/CHANGES.md +5 -0
  4. data/README.md +93 -22
  5. data/lib/trailblazer/finder.rb +3 -0
  6. data/lib/trailblazer/finder/adapters/active_record.rb +4 -0
  7. data/lib/trailblazer/finder/adapters/active_record/predicates.rb +42 -0
  8. data/lib/trailblazer/finder/adapters/friendly_id.rb +22 -24
  9. data/lib/trailblazer/finder/adapters/sequel.rb +5 -0
  10. data/lib/trailblazer/finder/adapters/sequel/predicates.rb +43 -0
  11. data/lib/trailblazer/finder/base.rb +3 -3
  12. data/lib/trailblazer/finder/features.rb +1 -0
  13. data/lib/trailblazer/finder/features/predicate.rb +37 -0
  14. data/lib/trailblazer/finder/filter.rb +2 -2
  15. data/lib/trailblazer/finder/predicates.rb +39 -0
  16. data/lib/trailblazer/finder/utils/deep_locate.rb +45 -0
  17. data/lib/trailblazer/finder/utils/extra.rb +1 -1
  18. data/lib/trailblazer/finder/utils/splitter.rb +42 -0
  19. data/lib/trailblazer/finder/version.rb +1 -1
  20. data/spec/spec_helper_sequel.rb +1 -0
  21. data/spec/trailblazer/finder/adapters/active_record/base_spec.rb +1 -1
  22. data/spec/trailblazer/finder/adapters/active_record/predicates_spec.rb +98 -0
  23. data/spec/trailblazer/finder/adapters/active_record/sorting_spec.rb +4 -0
  24. data/spec/trailblazer/finder/adapters/data_mapper/base_spec.rb +1 -1
  25. data/spec/trailblazer/finder/adapters/sequel/base_spec.rb +1 -1
  26. data/spec/trailblazer/finder/adapters/sequel/predicates_spec.rb +89 -0
  27. data/spec/trailblazer/finder/adapters/sequel/sorting_spec.rb +4 -0
  28. data/spec/trailblazer/finder/features/paging_spec.rb +1 -1
  29. data/spec/trailblazer/finder/features/predicates_spec.rb +99 -0
  30. data/spec/trailblazer/finder/features/sorting_spec.rb +3 -4
  31. data/spec/trailblazer/operation/predicates_spec.rb +127 -0
  32. data/spec/trailblazer/test_spec.rb +41 -0
  33. data/trailblazer-finder.gemspec +0 -2
  34. metadata +17 -15
@@ -1,31 +1,29 @@
1
- if Gem.loaded_specs.key?('activerecord')
2
- module Trailblazer
3
- class Finder
4
- module Adapters
5
- # FriendlyId Adapter
6
- module FriendlyId
7
- def self.included(base)
8
- base.extend ClassMethods
9
- base.class_eval do
10
- filter_by :id, with: :apply_slug_filter
11
- end
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ # FriendlyId Adapter
5
+ module FriendlyId
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.class_eval do
9
+ filter_by :id, with: :apply_slug_filter
12
10
  end
11
+ end
13
12
 
14
- def apply_slug_filter(entity_type, value)
15
- return if value.nil?
16
- return if value == ''
17
- if num?(value)
18
- entity_type.where(id: value)
19
- else
20
- entity_type.where(slug: Utils::String.underscore(value.downcase))
21
- end
13
+ def apply_slug_filter(entity_type, value)
14
+ return if value.nil?
15
+ return if value == ''
16
+ if num?(value)
17
+ entity_type.where(id: value)
18
+ else
19
+ entity_type.where(slug: Utils::String.underscore(value.downcase))
22
20
  end
21
+ end
23
22
 
24
- def num?(str)
25
- Integer(str)
26
- rescue ArgumentError, TypeError
27
- false
28
- end
23
+ def num?(str)
24
+ Integer(str)
25
+ rescue ArgumentError, TypeError
26
+ false
29
27
  end
30
28
  end
31
29
  end
@@ -1,5 +1,6 @@
1
1
  require 'trailblazer/finder/adapters/sequel/paging'
2
2
  require 'trailblazer/finder/adapters/sequel/sorting'
3
+ require 'trailblazer/finder/adapters/sequel/predicates'
3
4
 
4
5
  module Trailblazer
5
6
  class Finder
@@ -8,6 +9,10 @@ module Trailblazer
8
9
  module Sequel
9
10
  def self.included(base)
10
11
  base.extend ClassMethods
12
+ base.instance_eval do
13
+ include Sequel::Predicates if defined?(Features::Predicate::ClassMethods)
14
+ end
15
+ # require 'trailblazer/finder/adapters/sequel/predicates' if defined?(Features::Predicates::ClassMethods)
11
16
  end
12
17
 
13
18
  module ClassMethods
@@ -0,0 +1,43 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Adapters
4
+ module Sequel
5
+ # Sequel - Predicates Adapter
6
+ module Predicates
7
+
8
+ def not_eq(attribute, value, entity_type)
9
+ entity_type.exclude(attribute.to_sym => value.to_s)
10
+ end
11
+
12
+ def eq(attribute, value, entity_type)
13
+ entity_type.where(attribute.to_sym => value.to_s)
14
+ end
15
+
16
+ def blank(attribute, value, entity_type)
17
+ entity_type.where(attribute.to_sym => nil).or(attribute.to_sym => "")
18
+ end
19
+
20
+ def not_blank(attribute, value, entity_type)
21
+ entity_type.exclude(attribute.to_sym => nil).exclude(attribute.to_sym => "")
22
+ end
23
+
24
+ def gt(attribute, value, entity_type)
25
+ entity_type.where{::Sequel[attribute.to_sym] > value.to_f}
26
+ end
27
+
28
+ def gte(attribute, value, entity_type)
29
+ entity_type.where{::Sequel[attribute.to_sym] >= value.to_f}
30
+ end
31
+
32
+ def lt(attribute, value, entity_type)
33
+ entity_type.where{::Sequel[attribute.to_sym] < value.to_f}
34
+ end
35
+
36
+ def lte(attribute, value, entity_type)
37
+ entity_type.where{::Sequel[attribute.to_sym] <= value.to_f}
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,9 +1,9 @@
1
- require 'hashie/extensions/deep_locate'
2
-
3
1
  module Trailblazer
4
2
  class Finder
5
3
  # Base module
6
4
  module Base
5
+ PREDICATES = %w[eq not_eq].freeze
6
+
7
7
  def self.included(base)
8
8
  base.extend ClassMethods
9
9
  base.instance_eval do
@@ -88,7 +88,7 @@ module Trailblazer
88
88
  else
89
89
  lambda do |entity_type, value|
90
90
  return if Utils::String.blank?(value)
91
- Hashie::Extensions::DeepLocate.deep_locate ->(k, v, _object) { k == key.to_sym && v == value }, entity_type
91
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == key.to_sym && v == value }, entity_type
92
92
  end
93
93
  end
94
94
  end
@@ -1,5 +1,6 @@
1
1
  require 'trailblazer/finder/features/paging'
2
2
  require 'trailblazer/finder/features/sorting'
3
+ require 'trailblazer/finder/features/predicate'
3
4
 
4
5
  module Trailblazer
5
6
  class Finder
@@ -0,0 +1,37 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Features
4
+ module Predicate
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.instance_eval do
8
+ include Predicates
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def do_filters(attribute, predicate)
14
+ filter_by "#{attribute}_#{predicate}" do |entity_type, value|
15
+ splitter = Utils::Splitter.new "#{attribute}_#{predicate}", value
16
+ splitter.split_key predicate.to_sym
17
+ send splitter.op, splitter.field, splitter.value, entity_type
18
+ end
19
+ end
20
+
21
+ def predicates_for(*attributes)
22
+ attributes.each do |attribute|
23
+ do_filters(attribute, "eq")
24
+ do_filters(attribute, "not_eq")
25
+ do_filters(attribute, "blank")
26
+ do_filters(attribute, "not_blank")
27
+ do_filters(attribute, "gt")
28
+ do_filters(attribute, "gte")
29
+ do_filters(attribute, "lt")
30
+ do_filters(attribute, "lte")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -21,7 +21,7 @@ module Trailblazer
21
21
  module Handler
22
22
  module_function
23
23
 
24
- def build(name, defined_bys) # rubocop:disable Metrics/MethodLength
24
+ def build(name, defined_bys)
25
25
  defined_bys = defined_bys.map(&:to_s)
26
26
  handler = self
27
27
  lambda do |entity_type, value|
@@ -35,7 +35,7 @@ module Trailblazer
35
35
  end
36
36
  end
37
37
 
38
- def apply_filter(object:, filter_by:, defined_bys:, entity_type:, value:) # rubocop:disable Metrics/MethodLength
38
+ def apply_filter(object:, filter_by:, defined_bys:, entity_type:, value:)
39
39
  return if value.nil? || value == ''
40
40
 
41
41
  unless defined_bys.include? value
@@ -0,0 +1,39 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Predicates
4
+ def not_eq(attribute, value, entity_type)
5
+ return if Utils::String.blank?(value.to_s)
6
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_s != value.to_s && v != nil }, entity_type
7
+ end
8
+
9
+ def eq(attribute, value, entity_type)
10
+ return if Utils::String.blank?(value.to_s)
11
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_s == value.to_s && v != nil }, entity_type
12
+ end
13
+
14
+ def blank(attribute, _value, entity_type)
15
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && Utils::String.blank?(v.to_s) }, entity_type
16
+ end
17
+
18
+ def not_blank(attribute, _value, entity_type)
19
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && !Utils::String.blank?(v.to_s) }, entity_type
20
+ end
21
+
22
+ def gt(attribute, value, entity_type)
23
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_i > value.to_f }, entity_type
24
+ end
25
+
26
+ def gte(attribute, value, entity_type)
27
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_i >= value.to_f }, entity_type
28
+ end
29
+
30
+ def lt(attribute, value, entity_type)
31
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_i < value.to_f }, entity_type
32
+ end
33
+
34
+ def lte(attribute, value, entity_type)
35
+ Utils::DeepLocate.deep_locate ->(k, v, _object) { k == attribute.to_sym && v.to_i <= value.to_f }, entity_type
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ module Trailblazer
2
+ class Finder
3
+ module Utils
4
+ module DeepLocate
5
+ def self.deep_locate(comparator, object)
6
+ comparator = _construct_key_comparator(comparator, object) unless comparator.respond_to?(:call)
7
+
8
+ _deep_locate(comparator, object)
9
+ end
10
+
11
+ def self._construct_key_comparator(search_key, object)
12
+ search_key = search_key.to_s if defined?(::ActiveSupport::HashWithIndifferentAccess) && object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
13
+ search_key = search_key.to_s if object.respond_to?(:indifferent_access?) && object.indifferent_access?
14
+
15
+ lambda do |non_callable_object|
16
+ ->(key, _, _) { key == non_callable_object }
17
+ end.call(search_key)
18
+ end
19
+
20
+ def self._deep_locate(comparator, object, result = [])
21
+ if object.is_a?(::Enumerable)
22
+ if object.any? { |value| _match_comparator?(value, comparator, object) }
23
+ result.push object
24
+ end
25
+ (object.respond_to?(:values) ? object.values : object.entries).each do |value|
26
+ _deep_locate(comparator, value, result)
27
+ end
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ def self._match_comparator?(value, comparator, object)
34
+ if object.is_a?(::Hash)
35
+ key, value = value
36
+ else
37
+ key = nil
38
+ end
39
+
40
+ comparator.call(key, value, object)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -11,7 +11,7 @@ module Trailblazer
11
11
  end
12
12
  end
13
13
 
14
- def self.deep_copy(object) # rubocop:disable Metrics/MethodLength
14
+ def self.deep_copy(object)
15
15
  case object
16
16
  when Array
17
17
  object.map { |element| deep_copy(element) }
@@ -0,0 +1,42 @@
1
+ module Trailblazer
2
+ class Finder
3
+ # Helper module
4
+ module Utils
5
+ class Splitter
6
+ def initialize( key, value )
7
+ @key, @value = key, value
8
+ end
9
+
10
+ attr_reader :key, :value
11
+
12
+ # split suffix from the key and store the two values as name and op
13
+ # return truthy if successful
14
+ def split_key( suffix )
15
+ rv = @key =~ /\A(?:(.*?)_)?(#{suffix})\z/
16
+ @field, @op = $1, $2
17
+ rv
18
+ end
19
+
20
+ alias_method :===, :split_key
21
+ alias_method :=~, :split_key
22
+
23
+ # return name if the split was successful, or fall back to key
24
+ # which is handy when none of the predicates match and so key
25
+ # is probably just a field name.
26
+ def field
27
+ (@field || @key)
28
+ end
29
+
30
+ # the operator, or predicate
31
+ def op
32
+ @op.to_sym
33
+ end
34
+
35
+ # the value
36
+ def value
37
+ @value.to_s
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
2
  class Finder
3
- VERSION = '0.1.1'.freeze
3
+ VERSION = '0.1.2'.freeze
4
4
  end
5
5
  end
@@ -3,6 +3,7 @@ require_relative 'support/paging_shared_example'
3
3
  require_relative 'support/sorting_shared_example'
4
4
  require 'sequel'
5
5
  Sequel::Model.plugin :timestamps
6
+
6
7
  DB = Sequel.sqlite
7
8
 
8
9
  DB.create_table :s_products do
@@ -16,7 +16,7 @@ module Trailblazer
16
16
  end
17
17
  end
18
18
 
19
- def finder_class(&block) # rubocop:disable Metrics/MethodLength
19
+ def finder_class(&block)
20
20
  define_finder_class do
21
21
  adapters ActiveRecord
22
22
 
@@ -0,0 +1,98 @@
1
+ require 'spec_helper_active_record'
2
+ # require 'spec_helper'
3
+
4
+ module Trailblazer
5
+ class Finder
6
+ module Adapters
7
+ module ActiveRecord
8
+ describe Predicates do
9
+ class TestFinder < Trailblazer::Finder
10
+ features Predicate
11
+ adapters ActiveRecord
12
+
13
+ entity_type { Product }
14
+
15
+ predicates_for :name, :price, :created_at
16
+
17
+ filter_by(:category) { |entity_type, _| entity_type.joins(:category) }
18
+ end
19
+
20
+ def finder_with_predicate(filter = nil, value = nil, filters = {})
21
+ TestFinder.new filter: { filter => value }.merge(filters)
22
+ end
23
+
24
+ describe 'equals' do
25
+ before do
26
+ Product.delete_all
27
+ Product.reset_pk_sequence
28
+ 10.times do |i|
29
+ next Product.create name: "", price: "1#{i}" if i == 7
30
+ next Product.create name: nil, price: "1#{i}" if i == 8
31
+ next Product.create name: "product_4", price: "1#{i}" if i == 9
32
+ Product.create name: "product_#{i}", price: "1#{i}"
33
+ end
34
+ end
35
+
36
+ it 'it finds single row with name equal to product_5' do
37
+ finder = finder_with_predicate 'name_eq', 'product_5'
38
+ expect(finder.results.map(&:price)).to eq [15]
39
+ end
40
+
41
+ it 'it finds multiple rows with name equal to product_4' do
42
+ finder = finder_with_predicate 'name_eq', 'product_4'
43
+ expect(finder.results.map(&:price)).to eq [14, 19]
44
+ end
45
+
46
+ it 'it finds rows with name not equal to product_4' do
47
+ finder = finder_with_predicate 'name_not_eq', 'product_4'
48
+ expect(finder.results.map(&:name)).to eq ["product_0", "product_1", "product_2", "product_3", "product_5", "product_6", ""]
49
+ expect(finder.results.map(&:price)).to eq [10, 11, 12, 13, 15, 16, 17]
50
+ end
51
+
52
+ it 'it finds multiple rows with name blank (empty/nil)' do
53
+ finder = finder_with_predicate 'name_blank', ''
54
+ expect(finder.results.map(&:price)).to eq [17, 18]
55
+ end
56
+
57
+ it 'it finds multiple rows with name not blank (empty/nil)' do
58
+ finder = finder_with_predicate 'name_not_blank', ''
59
+ expect(finder.results.map(&:name)).to eq ["product_0", "product_1", "product_2", "product_3", "product_4", "product_5", "product_6", "product_4"]
60
+ end
61
+
62
+ it 'it finds multiple rows with price less than 15' do
63
+ finder = finder_with_predicate 'price_lt', 15
64
+ expect(finder.results.map(&:name)).to eq ["product_0", "product_1", "product_2", "product_3", "product_4"]
65
+ expect(finder.results.map(&:price)).to eq [10, 11, 12, 13, 14]
66
+ end
67
+
68
+ it 'it finds multiple rows with price equal to and less than 15' do
69
+ finder = finder_with_predicate 'price_lte', 15
70
+ expect(finder.results.map(&:name)).to eq ["product_0", "product_1", "product_2", "product_3", "product_4", "product_5"]
71
+ expect(finder.results.map(&:price)).to eq [10, 11, 12, 13, 14, 15]
72
+ end
73
+
74
+ it 'it finds multiple rows with price greater than 15' do
75
+ finder = finder_with_predicate 'price_gt', 15
76
+ expect(finder.results.map(&:name)).to eq ["product_6", "", nil, "product_4"]
77
+ expect(finder.results.map(&:price)).to eq [16, 17, 18, 19]
78
+ end
79
+
80
+ it 'it finds multiple rows with price equal to and greater than 15' do
81
+ finder = finder_with_predicate 'price_gte', 15
82
+ expect(finder.results.map(&:name)).to eq ["product_5", "product_6", "", nil, "product_4"]
83
+ expect(finder.results.map(&:price)).to eq [15, 16, 17, 18, 19]
84
+ end
85
+
86
+ it 'it finds rows with name not equal to product_4 and name not blank and price greater than 14' do
87
+ params = { name_not_blank: true, price_gt: 14 }
88
+ finder = finder_with_predicate 'name_not_eq', 'product_4', params
89
+
90
+ expect(finder.results.map(&:name)).to eq ["product_5", "product_6"]
91
+ expect(finder.results.map(&:price)).to eq [15, 16]
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end