actionset 0.8.1 → 0.11.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -5
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +1 -1
  5. data/CHANGELOG +67 -0
  6. data/Gemfile.lock +134 -126
  7. data/README.md +26 -0
  8. data/Rakefile +8 -1
  9. data/actionset.gemspec +2 -2
  10. data/lib/action_set.rb +8 -23
  11. data/lib/action_set/attribute_value.rb +7 -1
  12. data/lib/action_set/filter_instructions.rb +72 -0
  13. data/lib/action_set/helpers/helper_methods.rb +2 -1
  14. data/lib/action_set/helpers/pagination/record_description_for_helper.rb +20 -0
  15. data/lib/action_set/helpers/pagination/record_first_for_helper.rb +20 -0
  16. data/lib/action_set/helpers/pagination/record_last_for_helper.rb +26 -0
  17. data/lib/action_set/helpers/pagination/record_range_for_helper.rb +25 -0
  18. data/lib/action_set/helpers/pagination/record_size_for_helper.rb +9 -0
  19. data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +3 -1
  20. data/lib/action_set/sort_instructions.rb +44 -0
  21. data/lib/active_set.rb +25 -2
  22. data/lib/active_set/active_record_set_instruction.rb +33 -32
  23. data/lib/active_set/attribute_instruction.rb +3 -3
  24. data/lib/active_set/enumerable_set_instruction.rb +13 -24
  25. data/lib/active_set/filtering/active_record/operators.rb +280 -0
  26. data/lib/active_set/filtering/active_record/query_column.rb +35 -0
  27. data/lib/active_set/filtering/active_record/query_value.rb +47 -0
  28. data/lib/active_set/filtering/active_record/set_instruction.rb +29 -0
  29. data/lib/active_set/filtering/active_record/strategy.rb +87 -0
  30. data/lib/active_set/filtering/constants.rb +349 -0
  31. data/lib/active_set/filtering/enumerable/operators.rb +308 -0
  32. data/lib/active_set/filtering/enumerable/set_instruction.rb +98 -0
  33. data/lib/active_set/filtering/enumerable/strategy.rb +90 -0
  34. data/lib/active_set/filtering/operation.rb +5 -8
  35. data/lib/active_set/paginating/active_record_strategy.rb +0 -2
  36. data/lib/active_set/sorting/active_record_strategy.rb +27 -3
  37. data/lib/active_set/sorting/enumerable_strategy.rb +12 -2
  38. data/lib/active_set/sorting/operation.rb +1 -2
  39. data/lib/helpers/flatten_keys_of.rb +53 -0
  40. data/lib/helpers/transform_to_sortable_numeric.rb +3 -3
  41. metadata +26 -13
  42. data/lib/active_set/filtering/active_record_strategy.rb +0 -85
  43. data/lib/active_set/filtering/enumerable_strategy.rb +0 -79
  44. data/lib/helpers/throws.rb +0 -19
  45. data/lib/patches/core_ext/hash/flatten_keys.rb +0 -58
@@ -1,8 +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
+ require_relative './enumerable/strategy'
5
+ require_relative './active_record/strategy'
6
6
 
7
7
  class ActiveSet
8
8
  module Filtering
@@ -12,14 +12,12 @@ class ActiveSet
12
12
  @instructions_hash = instructions_hash
13
13
  end
14
14
 
15
- # rubocop:disable Metrics/MethodLength
16
15
  def execute
17
- attribute_instructions = @instructions_hash
18
- .flatten_keys
16
+ attribute_instructions = flatten_keys_of(@instructions_hash)
19
17
  .map { |k, v| AttributeInstruction.new(k, v) }
20
18
 
21
19
  activerecord_filtered_set = attribute_instructions.reduce(@set) do |set, attribute_instruction|
22
- maybe_set_or_false = ActiveRecordStrategy.new(set, attribute_instruction).execute
20
+ maybe_set_or_false = ActiveRecord::Strategy.new(set, attribute_instruction).execute
23
21
  next set unless maybe_set_or_false
24
22
 
25
23
  attribute_instruction.processed = true
@@ -29,14 +27,13 @@ class ActiveSet
29
27
  return activerecord_filtered_set if attribute_instructions.all?(&:processed?)
30
28
 
31
29
  attribute_instructions.reject(&:processed?).reduce(activerecord_filtered_set) do |set, attribute_instruction|
32
- maybe_set_or_false = EnumerableStrategy.new(set, attribute_instruction).execute
30
+ maybe_set_or_false = Enumerable::Strategy.new(set, attribute_instruction).execute
33
31
  next set unless maybe_set_or_false
34
32
 
35
33
  attribute_instruction.processed = true
36
34
  maybe_set_or_false
37
35
  end
38
36
  end
39
- # rubocop:enable Metrics/MethodLength
40
37
 
41
38
  def operation_instructions
42
39
  @instructions_hash.symbolize_keys
@@ -10,8 +10,6 @@ class ActiveSet
10
10
 
11
11
  def execute
12
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
13
 
16
14
  @set.limit(@operation_instructions[:size]).offset(page_offset)
17
15
  end
@@ -41,17 +41,23 @@ class ActiveSet
41
41
  private
42
42
 
43
43
  # https://stackoverflow.com/a/44912964/2884386
44
- # Force null values to be sorted as if larger than any non-null value
44
+ # When ActiveSet.configuration.on_asc_sort_nils_come == :last
45
+ # null values to be sorted as if larger than any non-null value.
45
46
  # ASC => [-2, -1, 1, 2, nil]
46
47
  # DESC => [nil, 2, 1, -1, -2]
48
+ # Otherwise sort nulls as if smaller than any non-null value.
49
+ # ASC => [nil, -2, -1, 1, 2]
50
+ # DESC => [2, 1, -1, -2, nil]
47
51
  def order_operation_for(set_instruction)
48
52
  attribute_model = set_instruction.attribute_model
49
53
 
50
54
  arel_column = set_instruction.arel_column
51
55
  arel_direction = direction_operator(set_instruction.value)
52
- nil_sorter = arel_column.send(arel_direction == :asc ? :eq : :not_eq, nil)
53
56
 
54
- attribute_model.order(nil_sorter).order(arel_column.send(arel_direction))
57
+ attribute_model.order(Arel.sql(nil_sorter_for(set_instruction.arel_table,
58
+ set_instruction.arel_column_name,
59
+ arel_direction)))
60
+ .order(arel_column.send(arel_direction))
55
61
  end
56
62
 
57
63
  def direction_operator(direction)
@@ -59,6 +65,24 @@ class ActiveSet
59
65
 
60
66
  :asc
61
67
  end
68
+
69
+ def nil_sorter_for(model, column, direction)
70
+ "CASE WHEN #{model.table_name}.#{column} IS NULL #{nil_sorter_then_statement(direction)}"
71
+ end
72
+
73
+ def nil_sorter_then_statement(direction)
74
+ first = 'THEN 0 ELSE 1 END'
75
+ last = 'THEN 1 ELSE 0 END'
76
+ if ActiveSet.configuration.on_asc_sort_nils_come == :last
77
+ return last if direction == :asc
78
+
79
+ return first
80
+ else
81
+ return first if direction == :asc
82
+
83
+ return last
84
+ end
85
+ end
62
86
  end
63
87
  end
64
88
  end
@@ -21,11 +21,15 @@ class ActiveSet
21
21
  value_for_comparison = sortable_numeric_for(set_instruction, item)
22
22
  direction_multiplier = direction_multiplier(set_instruction.value)
23
23
 
24
- # Force null values to be sorted as if larger than any non-null value
24
+ # When ActiveSet.configuration.on_asc_sort_nils_come == :last
25
+ # null values to be sorted as if larger than any non-null value.
25
26
  # ASC => [-2, -1, 1, 2, nil]
26
27
  # DESC => [nil, 2, 1, -1, -2]
28
+ # Otherwise sort nulls as if smaller than any non-null value.
29
+ # ASC => [nil, -2, -1, 1, 2]
30
+ # DESC => [2, 1, -1, -2, nil]
27
31
  if value_for_comparison.nil?
28
- [direction_multiplier, 0]
32
+ [direction_multiplier * nil_sorter, 0]
29
33
  else
30
34
  [0, value_for_comparison * direction_multiplier]
31
35
  end
@@ -44,6 +48,12 @@ class ActiveSet
44
48
 
45
49
  1
46
50
  end
51
+
52
+ def nil_sorter
53
+ return 1 if ActiveSet.configuration.on_asc_sort_nils_come == :last
54
+
55
+ -1
56
+ end
47
57
  end
48
58
  end
49
59
  end
@@ -13,8 +13,7 @@ class ActiveSet
13
13
  end
14
14
 
15
15
  def execute
16
- attribute_instructions = @instructions_hash
17
- .flatten_keys
16
+ attribute_instructions = flatten_keys_of(@instructions_hash)
18
17
  .map { |k, v| AttributeInstruction.new(k, v) }
19
18
 
20
19
  activerecord_strategy = ActiveRecordStrategy.new(@set, attribute_instructions)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/array/wrap'
4
+
5
+ # Returns a flat hash where all nested keys are flattened into an array of keys.
6
+ #
7
+ # hash = { key: 'value', nested: { key: 'nested_value' }, array: [0, 1, 2] }
8
+ # flatten_keys_of(hash)
9
+ # => { [:key]=>"value", [:nested, :key]=>"nested_value", [:array]=>[0, 1, 2] }
10
+ #
11
+ # Can also pass a Proc to change how nested keys are flattened:
12
+ # flatten_keys_of(hash, flattener: ->(*keys) { keys.join('.') })
13
+ # => { "key"=>"value", "nested.key"=>"nested_value", "array"=>[0, 1, 2] }
14
+ # flatten_keys_of(hash, flattener: ->(*keys) { keys.join('-') })
15
+ # => { "key"=>"value", "nested-key"=>"nested_value", "array"=>[0, 1, 2] }
16
+ # flatten_keys_of(hash, flattener: ->(*keys) { keys.map(&:to_s).reduce { |memo, key| memo + "[#{key}]" } })
17
+ # => { "key"=>"value", "nested[key]"=>"nested_value", "array"=>[0, 1, 2] }
18
+ #
19
+ # Can also determine if array values should be flattened as well:
20
+ # hash = { person: { age: '28', siblings: ['Tom', 'Sally'] } }
21
+ # flatten_keys_of(hash, flatten_arrays: true)
22
+ # => { [:key]=>"value", [:nested, :key]=>"nested_value", [:array, 0]=>0, [:array, 1]=>1, [:array, 2]=>2 }
23
+ # flatten_keys_of(hash, flattener: ->(*keys) { keys.join('.') }, flatten_arrays: true)
24
+ # => { "key"=>"value", "nested.key"=>"nested_value", "array.0"=>0, "array.1"=>1, "array.2"=>2 }
25
+
26
+ # refactored from https://stackoverflow.com/a/23861946/2884386
27
+ def flatten_keys_of(input, keys = [], output = {}, flattener: ->(*k) { k }, flatten_arrays: false)
28
+ if input.is_a?(Hash)
29
+ input.each do |key, value|
30
+ flatten_keys_of(
31
+ value,
32
+ keys + Array.wrap(key),
33
+ output,
34
+ flattener: flattener,
35
+ flatten_arrays: flatten_arrays
36
+ )
37
+ end
38
+ elsif input.is_a?(Array) && flatten_arrays
39
+ input.each_with_index do |value, index|
40
+ flatten_keys_of(
41
+ value,
42
+ keys + Array.wrap(index),
43
+ output,
44
+ flattener: flattener,
45
+ flatten_arrays: flatten_arrays
46
+ )
47
+ end
48
+ else
49
+ return output.merge!(flattener.call(*keys) => input)
50
+ end
51
+
52
+ output
53
+ end
@@ -13,7 +13,7 @@
13
13
  # transform_to_sortable_numeric(Date.new(2000, 12, 25))
14
14
  # => 977720400000
15
15
 
16
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
16
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
17
17
  def transform_to_sortable_numeric(value)
18
18
  # https://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/#and-what-about-parameters
19
19
  @sortable_numeric ||= Hash.new do |h, key|
@@ -35,8 +35,7 @@ def transform_to_sortable_numeric(value)
35
35
  end
36
36
  @sortable_numeric[value]
37
37
  end
38
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
39
- # rubocop:enable Style/AsciiComments
38
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
40
39
 
41
40
  def string_to_sortable_numeric(string)
42
41
  string # 'aB09ü'
@@ -46,6 +45,7 @@ def string_to_sortable_numeric(string)
46
45
  .reduce(&:concat) # "097.066048057252"
47
46
  .to_r # (24266512014313/250000000000)
48
47
  end
48
+ # rubocop:enable Style/AsciiComments
49
49
 
50
50
  def time_to_sortable_numeric(time)
51
51
  # https://stackoverflow.com/a/30604935/2884386
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionset
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-05-11 00:00:00.000000000 Z
11
+ date: 2021-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 5.1.0
145
+ version: 5.2.0
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 5.1.0
152
+ version: 5.2.0
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: rake
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -259,6 +259,7 @@ files:
259
259
  - config.ru
260
260
  - lib/action_set.rb
261
261
  - lib/action_set/attribute_value.rb
262
+ - lib/action_set/filter_instructions.rb
262
263
  - lib/action_set/helpers/export/path_for_helper.rb
263
264
  - lib/action_set/helpers/helper_methods.rb
264
265
  - lib/action_set/helpers/pagination/current_page_description_for_helper.rb
@@ -270,6 +271,11 @@ files:
270
271
  - lib/action_set/helpers/pagination/page_size_for_helper.rb
271
272
  - lib/action_set/helpers/pagination/path_for_helper.rb
272
273
  - lib/action_set/helpers/pagination/prev_page_link_for_helper.rb
274
+ - lib/action_set/helpers/pagination/record_description_for_helper.rb
275
+ - lib/action_set/helpers/pagination/record_first_for_helper.rb
276
+ - lib/action_set/helpers/pagination/record_last_for_helper.rb
277
+ - lib/action_set/helpers/pagination/record_range_for_helper.rb
278
+ - lib/action_set/helpers/pagination/record_size_for_helper.rb
273
279
  - lib/action_set/helpers/pagination/total_pages_for_helper.rb
274
280
  - lib/action_set/helpers/params/current_helper.rb
275
281
  - lib/action_set/helpers/params/form_for_object_helper.rb
@@ -279,6 +285,7 @@ files:
279
285
  - lib/action_set/helpers/sort/link_for_helper.rb
280
286
  - lib/action_set/helpers/sort/next_direction_for_helper.rb
281
287
  - lib/action_set/helpers/sort/path_for_helper.rb
288
+ - lib/action_set/sort_instructions.rb
282
289
  - lib/active_set.rb
283
290
  - lib/active_set/active_record_set_instruction.rb
284
291
  - lib/active_set/attribute_instruction.rb
@@ -286,8 +293,15 @@ files:
286
293
  - lib/active_set/enumerable_set_instruction.rb
287
294
  - lib/active_set/exporting/csv_strategy.rb
288
295
  - lib/active_set/exporting/operation.rb
289
- - lib/active_set/filtering/active_record_strategy.rb
290
- - lib/active_set/filtering/enumerable_strategy.rb
296
+ - lib/active_set/filtering/active_record/operators.rb
297
+ - lib/active_set/filtering/active_record/query_column.rb
298
+ - lib/active_set/filtering/active_record/query_value.rb
299
+ - lib/active_set/filtering/active_record/set_instruction.rb
300
+ - lib/active_set/filtering/active_record/strategy.rb
301
+ - lib/active_set/filtering/constants.rb
302
+ - lib/active_set/filtering/enumerable/operators.rb
303
+ - lib/active_set/filtering/enumerable/set_instruction.rb
304
+ - lib/active_set/filtering/enumerable/strategy.rb
291
305
  - lib/active_set/filtering/operation.rb
292
306
  - lib/active_set/paginating/active_record_strategy.rb
293
307
  - lib/active_set/paginating/enumerable_strategy.rb
@@ -295,15 +309,14 @@ files:
295
309
  - lib/active_set/sorting/active_record_strategy.rb
296
310
  - lib/active_set/sorting/enumerable_strategy.rb
297
311
  - lib/active_set/sorting/operation.rb
298
- - lib/helpers/throws.rb
312
+ - lib/helpers/flatten_keys_of.rb
299
313
  - lib/helpers/transform_to_sortable_numeric.rb
300
- - lib/patches/core_ext/hash/flatten_keys.rb
301
314
  - pagination.png
302
315
  homepage: https://github.com/fractaledmind/actionset
303
316
  licenses:
304
317
  - MIT
305
318
  metadata: {}
306
- post_install_message:
319
+ post_install_message:
307
320
  rdoc_options: []
308
321
  require_paths:
309
322
  - lib
@@ -318,9 +331,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
318
331
  - !ruby/object:Gem::Version
319
332
  version: '0'
320
333
  requirements: []
321
- rubyforge_project:
322
- rubygems_version: 2.6.11
323
- signing_key:
334
+ rubyforge_project:
335
+ rubygems_version: 2.6.14.4
336
+ signing_key:
324
337
  specification_version: 4
325
338
  summary: A toolkit for working with collections.
326
339
  test_files: []
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../active_record_set_instruction'
4
- require 'active_support/core_ext/module/delegation'
5
-
6
- class ActiveSet
7
- module Filtering
8
- class ActiveRecordStrategy
9
- delegate :attribute_model,
10
- :arel_column,
11
- :arel_operator,
12
- :arel_value,
13
- :arel_type,
14
- :initial_relation,
15
- :attribute,
16
- to: :@set_instruction
17
-
18
- def initialize(set, attribute_instruction)
19
- @set = set
20
- @attribute_instruction = attribute_instruction
21
- @set_instruction = ActiveRecordSetInstruction.new(attribute_instruction, set)
22
- end
23
-
24
- def execute
25
- return false unless @set.respond_to? :to_sql
26
-
27
- if execute_filter_operation?
28
- statement = filter_operation
29
- elsif execute_intersect_operation?
30
- begin
31
- statement = intersect_operation
32
- rescue ArgumentError # thrown if merging a non-ActiveRecord::Relation
33
- return false
34
- end
35
- else
36
- return false
37
- end
38
-
39
- statement
40
- end
41
-
42
- private
43
-
44
- def execute_filter_operation?
45
- return false unless attribute_model
46
- return false unless attribute_model.respond_to?(:attribute_names)
47
- return false unless attribute_model.attribute_names.include?(attribute)
48
-
49
- true
50
- end
51
-
52
- def execute_intersect_operation?
53
- return false unless attribute_model
54
- return false unless attribute_model.respond_to?(attribute)
55
- return false if attribute_model.method(attribute).arity.zero?
56
-
57
- true
58
- end
59
-
60
- def filter_operation
61
- initial_relation
62
- .where(
63
- arel_column.send(
64
- arel_operator,
65
- arel_value
66
- )
67
- )
68
- end
69
-
70
- def intersect_operation
71
- # NOTE: If merging relations that contain duplicate column conditions,
72
- # the second condition will replace the first.
73
- # e.g. Thing.where(id: [1,2]).merge(Thing.where(id: [2,3]))
74
- # => [Thing<2>, Thing<3>] NOT [Thing<2>]
75
- initial_relation
76
- .merge(
77
- attribute_model.public_send(
78
- attribute,
79
- arel_value
80
- )
81
- )
82
- end
83
- end
84
- end
85
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../enumerable_set_instruction'
4
- require 'active_support/core_ext/module/delegation'
5
-
6
- class ActiveSet
7
- module Filtering
8
- class EnumerableStrategy
9
- delegate :attribute_instance,
10
- :attribute_class,
11
- :attribute_value,
12
- :attribute_value_for,
13
- :operator,
14
- :attribute,
15
- to: :@set_instruction
16
-
17
- def initialize(set, attribute_instruction)
18
- @set = set
19
- @attribute_instruction = attribute_instruction
20
- @set_instruction = EnumerableSetInstruction.new(attribute_instruction, set)
21
- end
22
-
23
- def execute
24
- return false unless @set.respond_to? :select
25
-
26
- if execute_filter_operation?
27
- set = filter_operation
28
- elsif execute_intersect_operation?
29
- begin
30
- set = intersect_operation
31
- rescue TypeError # thrown if intersecting with a non-Array
32
- return false
33
- end
34
- else
35
- return false
36
- end
37
-
38
- set
39
- end
40
-
41
- private
42
-
43
- def execute_filter_operation?
44
- return false unless attribute_instance
45
- return false unless attribute_instance.respond_to?(attribute)
46
- return false if attribute_instance.method(attribute).arity.positive?
47
-
48
- true
49
- end
50
-
51
- def execute_intersect_operation?
52
- return false unless attribute_class
53
- return false unless attribute_class.respond_to?(attribute)
54
- return false if attribute_class.method(attribute).arity.zero?
55
-
56
- true
57
- end
58
-
59
- def filter_operation
60
- @set.select do |item|
61
- attribute_value_for(item)
62
- .public_send(
63
- operator,
64
- attribute_value
65
- )
66
- end
67
- end
68
-
69
- def intersect_operation
70
- other_set = attribute_class
71
- .public_send(
72
- attribute,
73
- attribute_value
74
- )
75
- @set & other_set
76
- end
77
- end
78
- end
79
- end