activeset 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2d87d34f54e2c174c5c11379a49ebd92d5f9b60c
4
- data.tar.gz: 5053ea2083a2eec445a06320427008452318c803
3
+ metadata.gz: b7d4ed6e6770fc71dc3a001a2dc9c19a871445e3
4
+ data.tar.gz: c1db7e7e4a0b3942563018e2404af6e645bcd68e
5
5
  SHA512:
6
- metadata.gz: 78f7b3a62321ac056cb550185b09b9e8b333b2723e938f1f38ec4d8bcc48c709e92f0a9c53107b3f733965edfadebd4e3ed55be13577e25a3510f9eab5d7c4c3
7
- data.tar.gz: c286846a5e54c850bc352d2c03f916e2048e4cbd03c69704fa7fa55dcd7bfb744212448c9744997d56da74c6c5ba3ff8d2ec851644ef224e58526662a0f70f34
6
+ metadata.gz: fd2718bbacff6f512f9ad90cb33e87a1b14eaadbe85774949e2df5b02439836e1e58a5b21286f6c9481b5efdb6239715d35465ab8341b5be2e907e2007f941c0
7
+ data.tar.gz: 833e98c0cf55ef9dc7229922070ba6e02f35ce2801728489f968ee08efaf689b62242592a22ca8cc23012263af4003c6f8de1213e6317b1c224e788919c9ff31
data/.hound.yml ADDED
@@ -0,0 +1,32 @@
1
+ rubocop:
2
+ config_file: .rubocop.yml
3
+
4
+ reek:
5
+ enabled: true
6
+
7
+ coffeescript:
8
+ enabled: false
9
+ credo:
10
+ enabled: false
11
+ css:
12
+ enabled: false
13
+ eslint:
14
+ enabled: false
15
+ flake8:
16
+ enabled: false
17
+ golint:
18
+ enabled: false
19
+ haml:
20
+ enabled: false
21
+ jshint:
22
+ enabled: false
23
+ sass-lint:
24
+ enabled: false
25
+ scss:
26
+ enabled: false
27
+ shellcheck:
28
+ enabled: false
29
+ swiftlint:
30
+ enabled: false
31
+ tslint:
32
+ enabled: false
data/.rubocop.yml CHANGED
@@ -17,3 +17,7 @@ Metrics/BlockLength:
17
17
  - 'activeset.gemspec'
18
18
  - 'spec/**/*_spec.rb'
19
19
  - '**/schema.rb'
20
+ Naming/VariableNumber:
21
+ EnforcedStyle: snake_case
22
+ Style/DateTime:
23
+ AllowCoercion: true
data/.travis.yml CHANGED
@@ -1,5 +1,9 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.1
5
- before_install: gem install bundler -v 1.15.4
4
+ - 2.4.1
5
+ before_install:
6
+ - gem install bundler -v 1.15.4
7
+ - export TZ=America/New_York
8
+ after_success:
9
+ - bash < (curl -s https://codecov.io/bash)
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ v 0.8.0
2
+ - simplify the `inspect` method for ActiveSet instances, only showing the instructions
3
+ - split the strategy files from the operation files
4
+ - update how ActiveRecord filtering works
5
+ + instruction operators must now be ARel methods, not SQL operators
6
+ - memoize the `transform_to_sortable_numeric` method
1
7
  v 0.7.1
2
8
  - Fix a small bug with Enumberable filtering of metaprogrammatically defined getter methods
3
9
  v 0.7.0
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # ActiveSet
2
2
 
3
+ [![Build Status](https://travis-ci.com/fractaledmind/activeset.svg?branch=master)](https://travis-ci.com/fractaledmind/activeset)
4
+ [![codecov](https://codecov.io/gh/fractaledmind/activeset/branch/master/graph/badge.svg)](https://codecov.io/gh/fractaledmind/activeset)
5
+
3
6
  ## Installation
4
7
 
5
8
  Add this line to your application's Gemfile:
data/activeset.gemspec CHANGED
@@ -6,7 +6,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
  Gem::Specification.new do |spec|
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.name = 'activeset'
9
- spec.version = '0.7.1'
9
+ spec.version = '0.8.0'
10
10
  spec.authors = ['Stephen Margheim']
11
11
  spec.email = ['stephen.margheim@gmail.com']
12
12
 
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'activesupport', '>= 4.0.2'
26
26
 
27
27
  spec.add_development_dependency 'bundler', '~> 1.15'
28
+ spec.add_development_dependency 'codecov'
28
29
  spec.add_development_dependency 'combustion', '~> 0.7.0'
29
30
  spec.add_development_dependency 'database_cleaner', '~> 1.6.1'
30
31
  spec.add_development_dependency 'factory_bot', '~> 4.8.0'
data/bin/console CHANGED
@@ -4,20 +4,21 @@
4
4
  ENV['RAILS_ENV'] ||= 'test'
5
5
 
6
6
  require 'bundler'
7
+ require 'simplecov'
8
+ require 'codecov'
9
+ require 'combustion'
10
+
11
+ Combustion.initialize! :active_record, :action_controller, :action_view
7
12
  Bundler.require :default, :development
8
13
 
9
14
  require 'bundler/setup'
10
15
  require 'active_set'
11
16
  require 'ostruct'
12
17
 
13
- # You can add fixtures and/or initialization code here to make experimenting
14
- # with your gem easier. You can also use a different console, if you like.
15
-
16
- Combustion.initialize! :active_record
17
-
18
18
  begin
19
19
  FactoryBot.find_definitions
20
20
  rescue FactoryBot::DuplicateDefinitionError
21
+ nil
21
22
  end
22
23
 
23
24
  # (If you use this, don't forget to add pry to your Gemfile!)
data/lib/active_set.rb CHANGED
@@ -26,7 +26,7 @@ class ActiveSet
26
26
 
27
27
  # :nocov:
28
28
  def inspect
29
- "#<ActiveSet:#{format('0x00%x', (object_id << 1))} @instructions=#{@instructions.inspect}>"
29
+ "#<ActiveSet:#{object_id} @instructions=#{@instructions.inspect}>"
30
30
  end
31
31
 
32
32
  def ==(other)
@@ -12,12 +12,10 @@ class ActiveSet
12
12
  def key
13
13
  return @instructions_hash[:key] if @instructions_hash.key? :key
14
14
 
15
- titleized = attribute_instruction.keypath.map(&:titleize).join(' ')
16
- return titleized unless attribute_instruction.attribute
15
+ return titleized_attribute_key unless attribute_instruction.attribute
17
16
 
18
17
  attribute_resource = attribute_instruction.resource_for(item: @item)
19
- return titleized unless attribute_resource
20
- return titleized unless attribute_resource.class.respond_to?(:human_attribute_name)
18
+ return titleized_attribute_key unless attribute_resource&.class&.respond_to?(:human_attribute_name)
21
19
 
22
20
  attribute_resource.class.human_attribute_name(attribute_instruction.attribute)
23
21
  end
@@ -40,5 +38,9 @@ class ActiveSet
40
38
 
41
39
  '—'
42
40
  end
41
+
42
+ def titleized_attribute_key
43
+ attribute_instruction.keypath.map(&:titleize).join(' ')
44
+ end
43
45
  end
44
46
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../column_instruction'
4
+
5
+ class ActiveSet
6
+ module Exporting
7
+ class CSVStrategy
8
+ require 'csv'
9
+
10
+ def initialize(set, column_instructions)
11
+ @set = set
12
+ @column_instructions = column_instructions
13
+ end
14
+
15
+ def execute
16
+ ::CSV.generate do |output|
17
+ output << column_keys_for(item: @set.first)
18
+ @set.each do |item|
19
+ output << column_values_for(item: item)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def column_keys_for(item:)
27
+ columns.map do |column|
28
+ ColumnInstruction.new(column, item).key
29
+ end
30
+ end
31
+
32
+ def column_values_for(item:)
33
+ columns.map do |column|
34
+ ColumnInstruction.new(column, item).value
35
+ end
36
+ end
37
+
38
+ def columns
39
+ @column_instructions.compact
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../column_instruction'
3
+ require_relative './csv_strategy'
4
4
 
5
5
  class ActiveSet
6
6
  module Exporting
@@ -26,41 +26,5 @@ class ActiveSet
26
26
  return CSVStrategy if format == 'csv'
27
27
  end
28
28
  end
29
-
30
- class CSVStrategy
31
- require 'csv'
32
-
33
- def initialize(set, column_instructions)
34
- @set = set
35
- @column_instructions = column_instructions
36
- end
37
-
38
- def execute
39
- ::CSV.generate do |output|
40
- output << column_keys_for(item: @set.first)
41
- @set.each do |item|
42
- output << column_values_for(item: item)
43
- end
44
- end
45
- end
46
-
47
- private
48
-
49
- def column_keys_for(item:)
50
- columns.map do |column|
51
- ColumnInstruction.new(column, item).key
52
- end
53
- end
54
-
55
- def column_values_for(item:)
56
- columns.map do |column|
57
- ColumnInstruction.new(column, item).value
58
- end
59
- end
60
-
61
- def columns
62
- @column_instructions.compact
63
- end
64
- end
65
29
  end
66
30
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Filtering
5
+ class ActiveRecordStrategy
6
+ def initialize(set, attribute_instruction)
7
+ @set = set
8
+ @attribute_instruction = attribute_instruction
9
+ end
10
+
11
+ def execute
12
+ return false unless @set.respond_to? :to_sql
13
+
14
+ if execute_where_operation?
15
+ statement = where_operation
16
+ elsif execute_merge_operation?
17
+ statement = merge_operation
18
+ else
19
+ return false
20
+ end
21
+
22
+ return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
23
+
24
+ statement
25
+ end
26
+
27
+ private
28
+
29
+ def execute_where_operation?
30
+ return false unless attribute_model
31
+ return false unless attribute_model.respond_to?(:attribute_names)
32
+ return false unless attribute_model.attribute_names.include?(@attribute_instruction.attribute)
33
+
34
+ true
35
+ end
36
+
37
+ def execute_merge_operation?
38
+ return false unless attribute_model
39
+ return false unless attribute_model.respond_to?(@attribute_instruction.attribute)
40
+ return false if attribute_model.method(@attribute_instruction.attribute).arity.zero?
41
+
42
+ true
43
+ end
44
+
45
+ def where_operation
46
+ arel_table = Arel::Table.new(attribute_model.table_name)
47
+ arel_column = arel_table[@attribute_instruction.attribute]
48
+
49
+ initial_relation
50
+ .where(
51
+ arel_column.send(
52
+ @attribute_instruction.operator(default: 'eq'),
53
+ @attribute_instruction.value
54
+ )
55
+ )
56
+ end
57
+
58
+ def merge_operation
59
+ initial_relation
60
+ .merge(
61
+ attribute_model.public_send(
62
+ @attribute_instruction.attribute,
63
+ @attribute_instruction.value
64
+ )
65
+ )
66
+ end
67
+
68
+ def initial_relation
69
+ return @set if @attribute_instruction.associations_array.empty?
70
+
71
+ @set.eager_load(@attribute_instruction.associations_hash)
72
+ end
73
+
74
+ def attribute_model
75
+ return @set.klass if @attribute_instruction.associations_array.empty?
76
+ return @attribute_model if defined? @attribute_model
77
+
78
+ @attribute_model = @attribute_instruction
79
+ .associations_array
80
+ .reduce(@set) do |obj, assoc|
81
+ obj.reflections[assoc.to_s]&.klass
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Filtering
5
+ class EnumerableStrategy
6
+ def initialize(set, attribute_instruction)
7
+ @set = set
8
+ @attribute_instruction = attribute_instruction
9
+ end
10
+
11
+ def execute
12
+ return false unless @set.respond_to? :select
13
+
14
+ @set.select do |item|
15
+ next attribute_matches_for?(item) if can_match_attribute_for?(item)
16
+ next class_method_matches_for?(item) if can_match_class_method_for?(item)
17
+
18
+ next false
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def can_match_attribute_for?(item)
25
+ attribute_item = attribute_item_for(item)
26
+
27
+ return false unless attribute_item
28
+ return false unless attribute_item.respond_to?(@attribute_instruction.attribute)
29
+ return false if attribute_item.method(@attribute_instruction.attribute).arity.positive?
30
+
31
+ true
32
+ end
33
+
34
+ def can_match_class_method_for?(item)
35
+ attribute_item = attribute_item_for(item)
36
+
37
+ return false unless attribute_item
38
+ return false unless attribute_item.class
39
+ return false unless attribute_item.class.respond_to?(@attribute_instruction.attribute)
40
+ return false if attribute_item.class.method(@attribute_instruction.attribute).arity.zero?
41
+
42
+ true
43
+ end
44
+
45
+ def attribute_matches_for?(item)
46
+ @attribute_instruction
47
+ .value_for(item: item)
48
+ .public_send(
49
+ @attribute_instruction.operator,
50
+ @attribute_instruction.value
51
+ )
52
+ end
53
+
54
+ # rubocop:disable Metrics/MethodLength
55
+ def class_method_matches_for?(item)
56
+ maybe_item_or_collection_or_nil = attribute_item_for(item)
57
+ .class
58
+ .public_send(
59
+ @attribute_instruction.attribute,
60
+ @attribute_instruction.value
61
+ )
62
+ if maybe_item_or_collection_or_nil.nil?
63
+ false
64
+ elsif maybe_item_or_collection_or_nil.respond_to?(:each)
65
+ maybe_item_or_collection_or_nil.include? attribute_item_for(item)
66
+ else
67
+ maybe_item_or_collection_or_nil.present?
68
+ end
69
+ end
70
+ # rubocop:enable Metrics/MethodLength
71
+
72
+ def attribute_item_for(item)
73
+ @attribute_instruction
74
+ .resource_for(item: item)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../attribute_instruction'
4
+ require_relative './enumerable_strategy'
5
+ require_relative './active_record_strategy'
4
6
 
5
7
  class ActiveSet
6
8
  module Filtering
@@ -10,10 +12,11 @@ class ActiveSet
10
12
  @instructions_hash = instructions_hash
11
13
  end
12
14
 
15
+ # rubocop:disable Metrics/MethodLength
13
16
  def execute
14
17
  attribute_instructions = @instructions_hash
15
- .flatten_keys
16
- .map { |k, v| AttributeInstruction.new(k, v) }
18
+ .flatten_keys
19
+ .map { |k, v| AttributeInstruction.new(k, v) }
17
20
 
18
21
  activerecord_filtered_set = attribute_instructions.reduce(@set) do |set, attribute_instruction|
19
22
  maybe_set_or_false = ActiveRecordStrategy.new(set, attribute_instruction).execute
@@ -30,161 +33,11 @@ class ActiveSet
30
33
  maybe_set_or_false.presence || set
31
34
  end
32
35
  end
36
+ # rubocop:enable Metrics/MethodLength
33
37
 
34
38
  def operation_instructions
35
39
  @instructions_hash.symbolize_keys
36
40
  end
37
41
  end
38
-
39
- class EnumerableStrategy
40
- def initialize(set, attribute_instruction)
41
- @set = set
42
- @attribute_instruction = attribute_instruction
43
- end
44
-
45
- def execute
46
- return false unless @set.respond_to? :select
47
-
48
- @set.select do |item|
49
- if can_match_attribute_for?(item)
50
- next attribute_matches_for?(item)
51
- elsif can_match_class_method_for?(item)
52
- next class_method_matches_for?(item)
53
- else
54
- next false
55
- end
56
- end
57
- end
58
-
59
- private
60
-
61
- def can_match_attribute_for?(item)
62
- attribute_item = attribute_item_for(item)
63
-
64
- return false unless attribute_item
65
- return false unless attribute_item.respond_to?(@attribute_instruction.attribute)
66
- return false if attribute_item.method(@attribute_instruction.attribute).arity > 0
67
-
68
- true
69
- end
70
-
71
- def can_match_class_method_for?(item)
72
- attribute_item = attribute_item_for(item)
73
-
74
- return false unless attribute_item
75
- return false unless attribute_item.class
76
- return false unless attribute_item.class.respond_to?(@attribute_instruction.attribute)
77
- return false if attribute_item.class.method(@attribute_instruction.attribute).arity.zero?
78
-
79
- true
80
- end
81
-
82
- def attribute_matches_for?(item)
83
- @attribute_instruction
84
- .value_for(item: item)
85
- .public_send(
86
- @attribute_instruction.operator,
87
- @attribute_instruction.value
88
- )
89
- end
90
-
91
- def class_method_matches_for?(item)
92
- maybe_item_or_collection_or_nil = attribute_item_for(item)
93
- .class
94
- .public_send(
95
- @attribute_instruction.attribute,
96
- @attribute_instruction.value
97
- )
98
- if maybe_item_or_collection_or_nil.nil?
99
- false
100
- elsif maybe_item_or_collection_or_nil.respond_to?(:each)
101
- maybe_item_or_collection_or_nil.include? attribute_item_for(item)
102
- else
103
- maybe_item_or_collection_or_nil.present?
104
- end
105
- end
106
-
107
- def attribute_item_for(item)
108
- @attribute_instruction
109
- .resource_for(item: item)
110
- end
111
- end
112
-
113
- class ActiveRecordStrategy
114
- def initialize(set, attribute_instruction)
115
- @set = set
116
- @attribute_instruction = attribute_instruction
117
- end
118
-
119
- def execute
120
- return false unless @set.respond_to? :to_sql
121
-
122
- if execute_where_operation?
123
- statement = where_operation
124
- elsif execute_merge_operation?
125
- statement = merge_operation
126
- else
127
- return false
128
- end
129
-
130
- return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
131
-
132
- statement
133
- end
134
-
135
- private
136
-
137
- def execute_where_operation?
138
- return false unless attribute_model
139
- return false unless attribute_model.respond_to?(:attribute_names)
140
- return false unless attribute_model.attribute_names.include?(@attribute_instruction.attribute)
141
-
142
- true
143
- end
144
-
145
- def execute_merge_operation?
146
- return false unless attribute_model
147
- return false unless attribute_model.respond_to?(@attribute_instruction.attribute)
148
- return false if attribute_model.method(@attribute_instruction.attribute).arity.zero?
149
-
150
- true
151
- end
152
-
153
- def where_operation
154
- arel_operator = @attribute_instruction.operator(default: '=')
155
- arel_column = Arel::Table.new(attribute_model.table_name)[@attribute_instruction.attribute]
156
- arel_value = Arel.sql(ActiveRecord::Base.connection.quote(@attribute_instruction.value))
157
-
158
- @set.eager_load(@attribute_instruction.associations_hash)
159
- .where(
160
- Arel::Nodes::InfixOperation.new(
161
- arel_operator,
162
- arel_column,
163
- arel_value
164
- )
165
- )
166
- end
167
-
168
- def merge_operation
169
- @set.eager_load(@attribute_instruction.associations_hash)
170
- .merge(
171
- attribute_model.public_send(
172
- @attribute_instruction.attribute,
173
- @attribute_instruction.value
174
- )
175
- )
176
- end
177
-
178
- def attribute_model
179
- return @set.klass if @attribute_instruction.associations_array.empty?
180
- return @attribute_model if defined? @attribute_model
181
-
182
- @attribute_model = @attribute_instruction
183
- .associations_array
184
- .reduce(@set) do |obj, assoc|
185
- obj.reflections[assoc.to_s]&.klass
186
- end
187
- end
188
- end
189
42
  end
190
43
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Paginating
5
+ class ActiveRecordStrategy
6
+ def initialize(set, operation_instructions)
7
+ @set = set
8
+ @operation_instructions = operation_instructions
9
+ end
10
+
11
+ def execute
12
+ return false unless @set.respond_to? :to_sql
13
+ return @set.none if @set.length <= @operation_instructions[:size] &&
14
+ @operation_instructions[:page] > 1
15
+
16
+ @set.limit(@operation_instructions[:size]).offset(page_offset)
17
+ end
18
+
19
+ private
20
+
21
+ def page_offset
22
+ return 0 if @operation_instructions[:page] == 1
23
+
24
+ @operation_instructions[:size] * (@operation_instructions[:page] - 1)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Paginating
5
+ class EnumerableStrategy
6
+ def initialize(set, operation_instructions)
7
+ @set = set
8
+ @operation_instructions = operation_instructions
9
+ end
10
+
11
+ def execute
12
+ return [] if @set.count <= @operation_instructions[:size] &&
13
+ @operation_instructions[:page] > 1
14
+
15
+ @set[page_start..page_end] || []
16
+ end
17
+
18
+ private
19
+
20
+ def page_start
21
+ return 0 if @operation_instructions[:page] == 1
22
+
23
+ @operation_instructions[:size] * (@operation_instructions[:page] - 1)
24
+ end
25
+
26
+ def page_end
27
+ return page_start if @operation_instructions[:size] == 1
28
+
29
+ page_start + @operation_instructions[:size] - 1
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './enumerable_strategy'
4
+ require_relative './active_record_strategy'
5
+
3
6
  class ActiveSet
4
7
  module Paginating
5
8
  class Operation
@@ -47,56 +50,5 @@ class ActiveSet
47
50
  maybe_integer_or_hash
48
51
  end
49
52
  end
50
-
51
- class EnumerableStrategy
52
- def initialize(set, operation_instructions)
53
- @set = set
54
- @operation_instructions = operation_instructions
55
- end
56
-
57
- def execute
58
- return [] if @set.count <= @operation_instructions[:size] &&
59
- @operation_instructions[:page] > 1
60
-
61
- @set[page_start..page_end] || []
62
- end
63
-
64
- private
65
-
66
- def page_start
67
- return 0 if @operation_instructions[:page] == 1
68
-
69
- @operation_instructions[:size] * (@operation_instructions[:page] - 1)
70
- end
71
-
72
- def page_end
73
- return page_start if @operation_instructions[:size] == 1
74
-
75
- page_start + @operation_instructions[:size] - 1
76
- end
77
- end
78
-
79
- class ActiveRecordStrategy
80
- def initialize(set, operation_instructions)
81
- @set = set
82
- @operation_instructions = operation_instructions
83
- end
84
-
85
- def execute
86
- return false unless @set.respond_to? :to_sql
87
- return @set.none if @set.length <= @operation_instructions[:size] &&
88
- @operation_instructions[:page] > 1
89
-
90
- @set.limit(@operation_instructions[:size]).offset(page_offset)
91
- end
92
-
93
- private
94
-
95
- def page_offset
96
- return 0 if @operation_instructions[:page] == 1
97
-
98
- @operation_instructions[:size] * (@operation_instructions[:page] - 1)
99
- end
100
- end
101
53
  end
102
54
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Sorting
5
+ class ActiveRecordStrategy
6
+ def initialize(set, attribute_instructions)
7
+ @set = set
8
+ @attribute_instructions = attribute_instructions
9
+ end
10
+
11
+ def execute
12
+ return false unless @set.respond_to? :to_sql
13
+
14
+ executable_instructions.reduce(set_with_eager_loaded_associations) do |set, attribute_instruction|
15
+ statement = set.merge(order_operation_for(attribute_instruction))
16
+
17
+ return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
18
+
19
+ attribute_instruction.processed = true
20
+ statement
21
+ end
22
+ end
23
+
24
+ def executable_instructions
25
+ return {} unless @set.respond_to? :to_sql
26
+
27
+ @attribute_instructions.select do |attribute_instruction|
28
+ attribute_model = attribute_model_for(attribute_instruction)
29
+ next false unless attribute_model
30
+ next false unless attribute_model.respond_to?(:attribute_names)
31
+ next false unless attribute_model.attribute_names.include?(attribute_instruction.attribute)
32
+
33
+ true
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def set_with_eager_loaded_associations
40
+ associations_hash = @attribute_instructions.reduce({}) { |h, i| h.merge(i.associations_hash) }
41
+ @set.eager_load(associations_hash)
42
+ end
43
+
44
+ def order_operation_for(attribute_instruction)
45
+ attribute_model = attribute_model_for(attribute_instruction)
46
+
47
+ arel_column = Arel::Table.new(attribute_model.table_name)[attribute_instruction.attribute]
48
+ arel_column = case_insensitive?(attribute_instruction) ? arel_column.lower : arel_column
49
+
50
+ arel_direction = direction_operator(attribute_instruction.value)
51
+
52
+ attribute_model.order(arel_column.send(arel_direction))
53
+ end
54
+
55
+ def attribute_model_for(attribute_instruction)
56
+ return @set.klass if attribute_instruction.associations_array.empty?
57
+
58
+ attribute_instruction
59
+ .associations_array
60
+ .reduce(@set) do |obj, assoc|
61
+ obj.reflections[assoc.to_s]&.klass
62
+ end
63
+ end
64
+
65
+ def case_insensitive?(attribute_instruction)
66
+ attribute_instruction.operator.to_s.casecmp('i').zero?
67
+ end
68
+
69
+ def direction_operator(direction)
70
+ return :desc if direction.to_s.downcase.start_with? 'desc'
71
+
72
+ :asc
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../helpers/transform_to_sortable_numeric'
4
+
5
+ class ActiveSet
6
+ module Sorting
7
+ class EnumerableStrategy
8
+ def initialize(set, attribute_instructions)
9
+ @set = set
10
+ @attribute_instructions = attribute_instructions
11
+ end
12
+
13
+ def execute
14
+ # http://brandon.dimcheff.com/2009/11/18/rubys-sort-vs-sort-by/
15
+ @set.sort_by do |item|
16
+ @attribute_instructions.map do |instruction|
17
+ sortable_numeric_for(instruction, item) * direction_multiplier(instruction.value)
18
+ end
19
+ end
20
+ end
21
+
22
+ def sortable_numeric_for(instruction, item)
23
+ value = instruction.value_for(item: item)
24
+ if value.is_a?(String) || value.is_a?(Symbol)
25
+ value = if case_insensitive?(instruction, value)
26
+ value.to_s.downcase
27
+ else
28
+ value.to_s
29
+ end
30
+ end
31
+
32
+ transform_to_sortable_numeric(value)
33
+ end
34
+
35
+ def case_insensitive?(instruction, _value)
36
+ instruction.operator.to_s.casecmp('i').zero?
37
+ end
38
+
39
+ def direction_multiplier(direction)
40
+ return -1 if direction.to_s.downcase.start_with? 'desc'
41
+
42
+ 1
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../attribute_instruction'
4
- require_relative '../../helpers/transform_to_sortable_numeric'
4
+ require_relative './enumerable_strategy'
5
+ require_relative './active_record_strategy'
5
6
 
6
7
  class ActiveSet
7
8
  module Sorting
@@ -13,8 +14,8 @@ class ActiveSet
13
14
 
14
15
  def execute
15
16
  attribute_instructions = @instructions_hash
16
- .flatten_keys
17
- .map { |k, v| AttributeInstruction.new(k, v) }
17
+ .flatten_keys
18
+ .map { |k, v| AttributeInstruction.new(k, v) }
18
19
 
19
20
  activerecord_strategy = ActiveRecordStrategy.new(@set, attribute_instructions)
20
21
  if activerecord_strategy.executable_instructions == attribute_instructions
@@ -30,115 +31,5 @@ class ActiveSet
30
31
  @instructions_hash.symbolize_keys
31
32
  end
32
33
  end
33
-
34
- class EnumerableStrategy
35
- def initialize(set, attribute_instructions)
36
- @set = set
37
- @attribute_instructions = attribute_instructions
38
- end
39
-
40
- def execute
41
- # http://brandon.dimcheff.com/2009/11/18/rubys-sort-vs-sort-by/
42
- @set.sort_by do |item|
43
- @attribute_instructions.map do |instruction|
44
- sortable_numeric_for(instruction, item) * direction_multiplier(instruction.value)
45
- end
46
- end
47
- end
48
-
49
- def sortable_numeric_for(instruction, item)
50
- value = instruction.value_for(item: item)
51
- if value.is_a?(String) || value.is_a?(Symbol)
52
- value = if case_insensitive?(instruction, value)
53
- value.to_s.downcase
54
- else
55
- value.to_s
56
- end
57
- end
58
-
59
- transform_to_sortable_numeric(value)
60
- end
61
-
62
- def case_insensitive?(instruction, _value)
63
- instruction.operator.to_s.casecmp('i').zero?
64
- end
65
-
66
- def direction_multiplier(direction)
67
- return -1 if direction.to_s.downcase.start_with? 'desc'
68
-
69
- 1
70
- end
71
- end
72
-
73
- class ActiveRecordStrategy
74
- def initialize(set, attribute_instructions)
75
- @set = set
76
- @attribute_instructions = attribute_instructions
77
- end
78
-
79
- def execute
80
- return false unless @set.respond_to? :to_sql
81
-
82
- executable_instructions.reduce(set_with_eager_loaded_associations) do |set, attribute_instruction|
83
- statement = set.merge(order_operation_for(attribute_instruction))
84
-
85
- return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
86
-
87
- attribute_instruction.processed = true
88
- statement
89
- end
90
- end
91
-
92
- def executable_instructions
93
- return {} unless @set.respond_to? :to_sql
94
-
95
- @attribute_instructions.select do |attribute_instruction|
96
- attribute_model = attribute_model_for(attribute_instruction)
97
- next false unless attribute_model
98
- next false unless attribute_model.respond_to?(:attribute_names)
99
- next false unless attribute_model.attribute_names.include?(attribute_instruction.attribute)
100
-
101
- true
102
- end
103
- end
104
-
105
- private
106
-
107
- def set_with_eager_loaded_associations
108
- associations_hash = @attribute_instructions.reduce({}) { |h, i| h.merge(i.associations_hash) }
109
- @set.eager_load(associations_hash)
110
- end
111
-
112
- def order_operation_for(attribute_instruction)
113
- attribute_model = attribute_model_for(attribute_instruction)
114
-
115
- arel_column = Arel::Table.new(attribute_model.table_name)[attribute_instruction.attribute]
116
- arel_column = case_insensitive?(attribute_instruction) ? arel_column.lower : arel_column
117
-
118
- arel_direction = direction_operator(attribute_instruction.value)
119
-
120
- attribute_model.order(arel_column.send(arel_direction))
121
- end
122
-
123
- def attribute_model_for(attribute_instruction)
124
- return @set.klass if attribute_instruction.associations_array.empty?
125
-
126
- attribute_instruction
127
- .associations_array
128
- .reduce(@set) do |obj, assoc|
129
- obj.reflections[assoc.to_s]&.klass
130
- end
131
- end
132
-
133
- def case_insensitive?(attribute_instruction)
134
- attribute_instruction.operator.to_s.casecmp('i').zero?
135
- end
136
-
137
- def direction_operator(direction)
138
- return :desc if direction.to_s.downcase.start_with? 'desc'
139
-
140
- :asc
141
- end
142
- end
143
34
  end
144
35
  end
@@ -11,9 +11,9 @@
11
11
  # throws?(StandardError) { 'foo' }
12
12
  # => false
13
13
 
14
- def throws?(exception) # &block
14
+ def throws?(exception)
15
15
  yield
16
16
  false
17
- rescue Exception => e
17
+ rescue StandardError => e
18
18
  e.is_a? exception
19
19
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Style/AsciiComments
3
4
  # Returns a Numeric for `value` that respects sort-order
4
5
  # can be used in Enumerable#sort_by
5
6
  #
@@ -12,25 +13,32 @@
12
13
  # transform_to_sortable_numeric(Date.new(2000, 12, 25))
13
14
  # => 977720400000
14
15
 
16
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
15
17
  def transform_to_sortable_numeric(value)
16
- return value if value.is_a?(Numeric)
17
- return 1 if value == true
18
- return 0 if value == false
19
-
20
- if value.is_a?(String) || value.is_a?(Symbol)
21
- return value
22
- .to_s # 'aB09ü'
23
- .split('') # ["a", "B", "0", "9", "ü"]
24
- .map { |char| char.ord.to_s.rjust(3, '0') } # ["097", "066", "048", "057", "252"]
25
- .insert(1, '.') # ["097", ".", "066", "048", "057", "252"]
26
- .reduce(&:concat) # "097.066048057252"
27
- .to_r # (24266512014313/250000000000)
18
+ # https://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/#and-what-about-parameters
19
+ @sortable_numeric ||= Hash.new do |h, key|
20
+ h[key] = if key.is_a?(Numeric)
21
+ key
22
+ elsif key == true
23
+ 1
24
+ elsif key == false
25
+ 0
26
+ elsif key.is_a?(String) || key.is_a?(Symbol)
27
+ key
28
+ .to_s # 'aB09ü'
29
+ .split('') # ["a", "B", "0", "9", "ü"]
30
+ .map { |char| char.ord.to_s.rjust(3, '0') } # ["097", "066", "048", "057", "252"]
31
+ .insert(1, '.') # ["097", ".", "066", "048", "057", "252"]
32
+ .reduce(&:concat) # "097.066048057252"
33
+ .to_r # (24266512014313/250000000000)
34
+ elsif key.respond_to?(:to_time)
35
+ # https://stackoverflow.com/a/30604935/2884386
36
+ (key.to_time.to_f * 1000).round
37
+ else
38
+ key
39
+ end
28
40
  end
29
-
30
- # https://stackoverflow.com/a/30604935/2884386
31
- return (value.to_time.to_f * 1000).round if value.respond_to?(:to_time)
32
-
33
- # :nocov:
34
- value
35
- # :nocov:
41
+ @sortable_numeric[value]
36
42
  end
43
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
44
+ # rubocop:enable Style/AsciiComments
@@ -8,8 +8,6 @@ class Hash
8
8
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
9
9
  # hash.flatten_keys_to_array
10
10
  # => {[:person, :name, :first] => "Rob", [:person, :age]=>"28" }
11
- # hash
12
- # => { person: { name: { first: 'Rob' }, age: '28' } }
13
11
  def flatten_keys_to_array
14
12
  _flatten_keys(self)
15
13
  end
@@ -19,9 +17,7 @@ class Hash
19
17
  #
20
18
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
21
19
  # hash.flatten_keys_to_dotpath
22
- # => { 'person.name.first' => "Rob", [:person, :age]=>"28" }
23
- # hash
24
- # => { person: { name: { first: 'Rob' }, age: '28' } }
20
+ # => { 'person.name.first' => "Rob", 'person.age'=>"28" }
25
21
  def flatten_keys_to_dotpath
26
22
  _flatten_keys(self, ->(*keys) { keys.join('.') })
27
23
  end
@@ -30,31 +26,32 @@ class Hash
30
26
  #
31
27
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
32
28
  # hash.flatten_keys_to_html_attribute
33
- # => { 'person-name-first' => "Rob", [:person, :age]=>"28" }
34
- # hash
35
- # => { person: { name: { first: 'Rob' }, age: '28' } }
29
+ # => { 'person-name-first' => "Rob", 'person-age'=>"28" }
36
30
  def flatten_keys_to_html_attribute
37
31
  _flatten_keys(self, ->(*keys) { keys.join('-') })
38
32
  end
39
33
 
40
- # Returns a flat hash where all nested keys are collapsed into a string of keys fitting the Rails request param pattern.
34
+ # Returns a flat hash where all nested keys are collapsed into a string of keys
35
+ # fitting the Rails request param pattern.
41
36
  #
42
37
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
43
38
  # hash.flatten_keys_to_rails_param
44
- # => { 'person[name][first]' => "Rob", [:person, :age]=>"28" }
45
- # hash
46
- # => { person: { name: { first: 'Rob' }, age: '28' } }
39
+ # => { 'person[name][first]' => "Rob", 'person[age]'=>"28" }
47
40
  def flatten_keys_to_rails_param
48
41
  _flatten_keys(self, ->(*keys) { keys.map(&:to_s).reduce { |memo, key| memo + "[#{key}]" } })
49
42
  end
50
43
 
51
44
  private
52
45
 
53
- # refactored little from https://stackoverflow.com/a/23861946/2884386
46
+ # refactored from https://stackoverflow.com/a/23861946/2884386
54
47
  def _flatten_keys(input, keypath_gen = ->(*keys) { keys }, keys = [], output = {})
55
- return output.merge!(keypath_gen.call(*keys) => input) unless input.is_a? Hash
56
-
57
- input.each { |k, v| _flatten_keys(v, keypath_gen, keys + Array(k), output) }
48
+ if input.is_a? Hash
49
+ input.each { |k, v| _flatten_keys(v, keypath_gen, keys + Array(k), output) }
50
+ # elsif input.is_a? Array
51
+ # input.each_with_index { |v, i| _flatten_keys(v, keypath_gen, keys + Array(i), output) }
52
+ else
53
+ return output.merge!(keypath_gen.call(*keys) => input)
54
+ end
58
55
 
59
56
  output
60
57
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeset
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-08 00:00:00.000000000 Z
11
+ date: 2018-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.15'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: combustion
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +200,7 @@ extensions: []
186
200
  extra_rdoc_files: []
187
201
  files:
188
202
  - ".gitignore"
203
+ - ".hound.yml"
189
204
  - ".rspec"
190
205
  - ".rubocop.yml"
191
206
  - ".ruby-version"
@@ -203,9 +218,16 @@ files:
203
218
  - lib/active_set.rb
204
219
  - lib/active_set/attribute_instruction.rb
205
220
  - lib/active_set/column_instruction.rb
221
+ - lib/active_set/exporting/csv_strategy.rb
206
222
  - lib/active_set/exporting/operation.rb
223
+ - lib/active_set/filtering/active_record_strategy.rb
224
+ - lib/active_set/filtering/enumerable_strategy.rb
207
225
  - lib/active_set/filtering/operation.rb
226
+ - lib/active_set/paginating/active_record_strategy.rb
227
+ - lib/active_set/paginating/enumerable_strategy.rb
208
228
  - lib/active_set/paginating/operation.rb
229
+ - lib/active_set/sorting/active_record_strategy.rb
230
+ - lib/active_set/sorting/enumerable_strategy.rb
209
231
  - lib/active_set/sorting/operation.rb
210
232
  - lib/helpers/throws.rb
211
233
  - lib/helpers/transform_to_sortable_numeric.rb