activeset 0.7.1 → 0.8.0

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