activeset 0.6.5 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +0 -6
  4. data/CHANGELOG +5 -0
  5. data/README.md +58 -6
  6. data/activeset.gemspec +8 -8
  7. data/bin/console +14 -0
  8. data/lib/.DS_Store +0 -0
  9. data/lib/active_set.rb +28 -21
  10. data/lib/active_set/{instruction.rb → attribute_instruction.rb} +12 -8
  11. data/lib/active_set/column_instruction.rb +44 -0
  12. data/lib/active_set/exporting/operation.rb +66 -0
  13. data/lib/active_set/filtering/operation.rb +190 -0
  14. data/lib/active_set/paginating/operation.rb +102 -0
  15. data/lib/active_set/sorting/operation.rb +144 -0
  16. data/lib/helpers/throws.rb +2 -2
  17. data/lib/helpers/transform_to_sortable_numeric.rb +36 -0
  18. data/lib/patches/core_ext/hash/flatten_keys.rb +43 -14
  19. metadata +55 -39
  20. data/lib/active_set/.DS_Store +0 -0
  21. data/lib/active_set/adapter_activerecord.rb +0 -58
  22. data/lib/active_set/adapter_base.rb +0 -14
  23. data/lib/active_set/instructions.rb +0 -42
  24. data/lib/active_set/processor_base.rb +0 -28
  25. data/lib/active_set/processor_filter.rb +0 -21
  26. data/lib/active_set/processor_filter/active_record_adapter.rb +0 -50
  27. data/lib/active_set/processor_filter/enumerable_adapter.rb +0 -21
  28. data/lib/active_set/processor_paginate.rb +0 -35
  29. data/lib/active_set/processor_paginate/active_record_adapter.rb +0 -37
  30. data/lib/active_set/processor_paginate/enumerable_adapter.rb +0 -39
  31. data/lib/active_set/processor_sort.rb +0 -19
  32. data/lib/active_set/processor_sort/active_record_adapter.rb +0 -27
  33. data/lib/active_set/processor_sort/active_record_operation.rb +0 -55
  34. data/lib/active_set/processor_sort/enumerable_adapter.rb +0 -58
  35. data/lib/active_set/processor_transform.rb +0 -30
  36. data/lib/active_set/processor_transform/csv_adapter.rb +0 -77
  37. data/lib/active_set/version.rb +0 -5
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveSet
4
+ module Paginating
5
+ class Operation
6
+ def initialize(set, instructions_hash)
7
+ @set = set
8
+ @instructions_hash = instructions_hash
9
+ end
10
+
11
+ def execute
12
+ [ActiveRecordStrategy, EnumerableStrategy].each do |strategy|
13
+ maybe_set_or_false = strategy.new(@set, operation_instructions).execute
14
+ break(maybe_set_or_false) if maybe_set_or_false
15
+ end
16
+ end
17
+
18
+ def operation_instructions
19
+ @instructions_hash.symbolize_keys.tap do |h|
20
+ h[:page] = page_operation_instruction(h[:page])
21
+ h[:size] = size_operation_instruction(h[:size])
22
+ h[:count] = count_operation_instruction(@set)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def page_operation_instruction(initial)
29
+ return 1 unless initial
30
+ return 1 if initial.to_i <= 0
31
+
32
+ initial.to_i
33
+ end
34
+
35
+ def size_operation_instruction(initial)
36
+ return 25 unless initial
37
+ return 25 if initial.to_i <= 0
38
+
39
+ initial.to_i
40
+ end
41
+
42
+ def count_operation_instruction(set)
43
+ # https://work.stevegrossi.com/2015/04/25/how-to-count-with-activerecord/
44
+ maybe_integer_or_hash = set.size
45
+ return maybe_integer_or_hash.count if maybe_integer_or_hash.is_a?(Hash)
46
+
47
+ maybe_integer_or_hash
48
+ end
49
+ 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
+ end
102
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../attribute_instruction'
4
+ require_relative '../../helpers/transform_to_sortable_numeric'
5
+
6
+ class ActiveSet
7
+ module Sorting
8
+ class Operation
9
+ def initialize(set, instructions_hash)
10
+ @set = set
11
+ @instructions_hash = instructions_hash
12
+ end
13
+
14
+ def execute
15
+ attribute_instructions = @instructions_hash
16
+ .flatten_keys
17
+ .map { |k, v| AttributeInstruction.new(k, v) }
18
+
19
+ activerecord_strategy = ActiveRecordStrategy.new(@set, attribute_instructions)
20
+ if activerecord_strategy.executable_instructions == attribute_instructions
21
+ activerecord_sorted_set = activerecord_strategy.execute
22
+ end
23
+
24
+ return activerecord_sorted_set if attribute_instructions.all?(&:processed?)
25
+
26
+ EnumerableStrategy.new(@set, attribute_instructions).execute
27
+ end
28
+
29
+ def operation_instructions
30
+ @instructions_hash.symbolize_keys
31
+ end
32
+ 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
+ end
144
+ end
@@ -13,7 +13,7 @@
13
13
 
14
14
  def throws?(exception) # &block
15
15
  yield
16
- return false
16
+ false
17
17
  rescue Exception => e
18
- return e.is_a? exception
18
+ e.is_a? exception
19
19
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns a Numeric for `value` that respects sort-order
4
+ # can be used in Enumerable#sort_by
5
+ #
6
+ # transform_to_sortable_numeric(1)
7
+ # => 1
8
+ # transform_to_sortable_numeric('aB09ü')
9
+ # => (24266512014313/250000000000)
10
+ # transform_to_sortable_numeric(true)
11
+ # => 1
12
+ # transform_to_sortable_numeric(Date.new(2000, 12, 25))
13
+ # => 977720400000
14
+
15
+ 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)
28
+ 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:
36
+ end
@@ -3,30 +3,59 @@
3
3
  require 'active_support/core_ext/array/wrap'
4
4
 
5
5
  class Hash
6
- # Returns a flat hash where all nested keys are collapsed into a an array of keys.
6
+ # Returns a flat hash where all nested keys are collapsed into an array of keys.
7
7
  #
8
8
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
9
- # hash.flatten_keys # => {[:person, :name, :first]=>"Rob", [:person, :age]=>"28"}
10
- # hash # => { person: { name: { first: 'Rob' }, age: '28' } }
11
- def flatten_keys
9
+ # hash.flatten_keys_to_array
10
+ # => {[:person, :name, :first] => "Rob", [:person, :age]=>"28" }
11
+ # hash
12
+ # => { person: { name: { first: 'Rob' }, age: '28' } }
13
+ def flatten_keys_to_array
12
14
  _flatten_keys(self)
13
15
  end
16
+ alias flatten_keys flatten_keys_to_array
14
17
 
15
- # Replaces current hash with a flat hash where all nested keys are collapsed into a an array of keys.
16
- # Returns +nil+ if no changes were made, otherwise returns the hash.
18
+ # Returns a flat hash where all nested keys are collapsed into a dot-separated string of keys.
17
19
  #
18
20
  # hash = { person: { name: { first: 'Rob' }, age: '28' } }
19
- # hash.flatten_keys! # => {[:person, :name, :first]=>"Rob", [:person, :age]=>"28"}
20
- # hash # => {[:person, :name, :first]=>"Rob", [:person, :age]=>"28"}
21
- def flatten_keys!
22
- replace(_flatten_keys(self))
21
+ # hash.flatten_keys_to_dotpath
22
+ # => { 'person.name.first' => "Rob", [:person, :age]=>"28" }
23
+ # hash
24
+ # => { person: { name: { first: 'Rob' }, age: '28' } }
25
+ def flatten_keys_to_dotpath
26
+ _flatten_keys(self, ->(*keys) { keys.join('.') })
27
+ end
28
+
29
+ # Returns a flat hash where all nested keys are collapsed into a dast-separated string of keys.
30
+ #
31
+ # hash = { person: { name: { first: 'Rob' }, age: '28' } }
32
+ # hash.flatten_keys_to_html_attribute
33
+ # => { 'person-name-first' => "Rob", [:person, :age]=>"28" }
34
+ # hash
35
+ # => { person: { name: { first: 'Rob' }, age: '28' } }
36
+ def flatten_keys_to_html_attribute
37
+ _flatten_keys(self, ->(*keys) { keys.join('-') })
38
+ end
39
+
40
+ # Returns a flat hash where all nested keys are collapsed into a string of keys fitting the Rails request param pattern.
41
+ #
42
+ # hash = { person: { name: { first: 'Rob' }, age: '28' } }
43
+ # hash.flatten_keys_to_rails_param
44
+ # => { 'person[name][first]' => "Rob", [:person, :age]=>"28" }
45
+ # hash
46
+ # => { person: { name: { first: 'Rob' }, age: '28' } }
47
+ def flatten_keys_to_rails_param
48
+ _flatten_keys(self, ->(*keys) { keys.map(&:to_s).reduce { |memo, key| memo + "[#{key}]" } })
23
49
  end
24
50
 
25
51
  private
26
52
 
27
- def _flatten_keys(h, keys = [], res = {})
28
- return res.merge!(keys => h) unless h.is_a? Hash
29
- h.each { |k, r| _flatten_keys(r, keys + Array.wrap(k), res) }
30
- res
53
+ # refactored little from https://stackoverflow.com/a/23861946/2884386
54
+ 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) }
58
+
59
+ output
31
60
  end
32
61
  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.6.5
4
+ version: 0.7.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-10-26 00:00:00.000000000 Z
11
+ date: 2018-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -39,89 +39,117 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.15'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: combustion
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: 0.7.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: 0.7.0
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec
56
+ name: database_cleaner
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.0'
61
+ version: 1.6.1
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.0'
68
+ version: 1.6.1
69
69
  - !ruby/object:Gem::Dependency
70
- name: database_cleaner
70
+ name: factory_bot
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.6.1
75
+ version: 4.8.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 1.6.1
82
+ version: 4.8.0
83
83
  - !ruby/object:Gem::Dependency
84
- name: combustion
84
+ name: faker
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.7.0
89
+ version: 1.8.4
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.7.0
96
+ version: 1.8.4
97
97
  - !ruby/object:Gem::Dependency
98
- name: factory_girl
98
+ name: rails
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 4.8.0
103
+ version: 5.1.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 4.8.0
110
+ version: 5.1.0
111
111
  - !ruby/object:Gem::Dependency
112
- name: faker
112
+ name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 1.8.4
117
+ version: '10.0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 1.8.4
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: simplecov
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -173,26 +201,14 @@ files:
173
201
  - config.ru
174
202
  - lib/.DS_Store
175
203
  - lib/active_set.rb
176
- - lib/active_set/.DS_Store
177
- - lib/active_set/adapter_activerecord.rb
178
- - lib/active_set/adapter_base.rb
179
- - lib/active_set/instruction.rb
180
- - lib/active_set/instructions.rb
181
- - lib/active_set/processor_base.rb
182
- - lib/active_set/processor_filter.rb
183
- - lib/active_set/processor_filter/active_record_adapter.rb
184
- - lib/active_set/processor_filter/enumerable_adapter.rb
185
- - lib/active_set/processor_paginate.rb
186
- - lib/active_set/processor_paginate/active_record_adapter.rb
187
- - lib/active_set/processor_paginate/enumerable_adapter.rb
188
- - lib/active_set/processor_sort.rb
189
- - lib/active_set/processor_sort/active_record_adapter.rb
190
- - lib/active_set/processor_sort/active_record_operation.rb
191
- - lib/active_set/processor_sort/enumerable_adapter.rb
192
- - lib/active_set/processor_transform.rb
193
- - lib/active_set/processor_transform/csv_adapter.rb
194
- - lib/active_set/version.rb
204
+ - lib/active_set/attribute_instruction.rb
205
+ - lib/active_set/column_instruction.rb
206
+ - lib/active_set/exporting/operation.rb
207
+ - lib/active_set/filtering/operation.rb
208
+ - lib/active_set/paginating/operation.rb
209
+ - lib/active_set/sorting/operation.rb
195
210
  - lib/helpers/throws.rb
211
+ - lib/helpers/transform_to_sortable_numeric.rb
196
212
  - lib/patches/core_ext/hash/flatten_keys.rb
197
213
  homepage: https://github.com/fractaledmind/activeset
198
214
  licenses: