active_fields 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +27 -2
  4. data/README.md +411 -38
  5. data/app/models/active_fields/field/boolean.rb +3 -0
  6. data/app/models/active_fields/field/date.rb +3 -0
  7. data/app/models/active_fields/field/date_array.rb +3 -0
  8. data/app/models/active_fields/field/date_time.rb +4 -1
  9. data/app/models/active_fields/field/date_time_array.rb +4 -1
  10. data/app/models/active_fields/field/decimal.rb +6 -1
  11. data/app/models/active_fields/field/decimal_array.rb +6 -1
  12. data/app/models/active_fields/field/enum.rb +3 -0
  13. data/app/models/active_fields/field/enum_array.rb +3 -0
  14. data/app/models/active_fields/field/integer.rb +3 -0
  15. data/app/models/active_fields/field/integer_array.rb +3 -0
  16. data/app/models/active_fields/field/text.rb +3 -0
  17. data/app/models/active_fields/field/text_array.rb +3 -0
  18. data/app/models/active_fields/field.rb +5 -0
  19. data/app/models/concerns/active_fields/customizable_concern.rb +89 -3
  20. data/app/models/concerns/active_fields/field_concern.rb +26 -3
  21. data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
  22. data/lib/active_fields/casters/date_time_caster.rb +1 -3
  23. data/lib/active_fields/casters/decimal_caster.rb +2 -5
  24. data/lib/active_fields/constants.rb +55 -0
  25. data/lib/active_fields/engine.rb +2 -1
  26. data/lib/active_fields/finders/array_finder.rb +112 -0
  27. data/lib/active_fields/finders/base_finder.rb +73 -0
  28. data/lib/active_fields/finders/boolean_finder.rb +20 -0
  29. data/lib/active_fields/finders/date_array_finder.rb +65 -0
  30. data/lib/active_fields/finders/date_finder.rb +32 -0
  31. data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
  32. data/lib/active_fields/finders/date_time_finder.rb +32 -0
  33. data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
  34. data/lib/active_fields/finders/decimal_finder.rb +32 -0
  35. data/lib/active_fields/finders/enum_array_finder.rb +41 -0
  36. data/lib/active_fields/finders/enum_finder.rb +20 -0
  37. data/lib/active_fields/finders/integer_array_finder.rb +65 -0
  38. data/lib/active_fields/finders/integer_finder.rb +32 -0
  39. data/lib/active_fields/finders/singular_finder.rb +66 -0
  40. data/lib/active_fields/finders/text_array_finder.rb +47 -0
  41. data/lib/active_fields/finders/text_finder.rb +81 -0
  42. data/lib/active_fields/has_active_fields.rb +3 -4
  43. data/lib/active_fields/registry.rb +38 -0
  44. data/lib/active_fields/version.rb +1 -1
  45. data/lib/active_fields.rb +29 -1
  46. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +9 -0
  47. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +0 -10
  48. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  49. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +67 -0
  50. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  51. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  52. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  53. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +1 -1
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +1 -1
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +1 -1
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +2 -2
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +2 -2
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +2 -2
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +2 -2
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +1 -1
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +1 -1
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +1 -1
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +1 -1
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +1 -1
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +1 -1
  79. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +1 -1
  80. metadata +42 -10
  81. data/lib/active_fields/customizable_config.rb +0 -24
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::DateCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::DateFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :min, :max
@@ -12,6 +12,9 @@ module ActiveFields
12
12
  caster: {
13
13
  class_name: "ActiveFields::Casters::DateArrayCaster",
14
14
  },
15
+ finder: {
16
+ class_name: "ActiveFields::Finders::DateArrayFinder",
17
+ },
15
18
  )
16
19
 
17
20
  store_accessor :options, :min, :max
@@ -12,6 +12,9 @@ module ActiveFields
12
12
  class_name: "ActiveFields::Casters::DateTimeCaster",
13
13
  options: -> { { precision: precision } },
14
14
  },
15
+ finder: {
16
+ class_name: "ActiveFields::Finders::DateTimeFinder",
17
+ },
15
18
  )
16
19
 
17
20
  store_accessor :options, :required, :min, :max, :precision
@@ -19,7 +22,7 @@ module ActiveFields
19
22
  validates :required, exclusion: [nil]
20
23
  validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
21
24
  validates :precision,
22
- comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: Casters::DateTimeCaster::MAX_PRECISION },
25
+ comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION },
23
26
  allow_nil: true
24
27
 
25
28
  # If precision is set after attributes that depend on it, deserialization will work correctly,
@@ -13,13 +13,16 @@ module ActiveFields
13
13
  class_name: "ActiveFields::Casters::DateTimeArrayCaster",
14
14
  options: -> { { precision: precision } },
15
15
  },
16
+ finder: {
17
+ class_name: "ActiveFields::Finders::DateTimeArrayFinder",
18
+ },
16
19
  )
17
20
 
18
21
  store_accessor :options, :min, :max, :precision
19
22
 
20
23
  validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
21
24
  validates :precision,
22
- comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: Casters::DateTimeCaster::MAX_PRECISION },
25
+ comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION },
23
26
  allow_nil: true
24
27
 
25
28
  # If precision is set after attributes that depend on it, deserialization will work correctly,
@@ -12,13 +12,18 @@ module ActiveFields
12
12
  class_name: "ActiveFields::Casters::DecimalCaster",
13
13
  options: -> { { precision: precision } },
14
14
  },
15
+ finder: {
16
+ class_name: "ActiveFields::Finders::DecimalFinder",
17
+ },
15
18
  )
16
19
 
17
20
  store_accessor :options, :required, :min, :max, :precision
18
21
 
19
22
  validates :required, exclusion: [nil]
20
23
  validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
21
- validates :precision, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true
24
+ validates :precision,
25
+ comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION },
26
+ allow_nil: true
22
27
 
23
28
  # If precision is set after attributes that depend on it, deserialization will work correctly,
24
29
  # but an incorrect internal value may be saved in the DB.
@@ -13,12 +13,17 @@ module ActiveFields
13
13
  class_name: "ActiveFields::Casters::DecimalArrayCaster",
14
14
  options: -> { { precision: precision } },
15
15
  },
16
+ finder: {
17
+ class_name: "ActiveFields::Finders::DecimalArrayFinder",
18
+ },
16
19
  )
17
20
 
18
21
  store_accessor :options, :min, :max, :precision
19
22
 
20
23
  validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
21
- validates :precision, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true
24
+ validates :precision,
25
+ comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION },
26
+ allow_nil: true
22
27
 
23
28
  # If precision is set after attributes that depend on it, deserialization will work correctly,
24
29
  # but an incorrect internal value may be saved in the DB.
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::EnumCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::EnumFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :allowed_values
@@ -12,6 +12,9 @@ module ActiveFields
12
12
  caster: {
13
13
  class_name: "ActiveFields::Casters::EnumArrayCaster",
14
14
  },
15
+ finder: {
16
+ class_name: "ActiveFields::Finders::EnumArrayFinder",
17
+ },
15
18
  )
16
19
 
17
20
  store_accessor :options, :allowed_values
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::IntegerCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::IntegerFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :min, :max
@@ -12,6 +12,9 @@ module ActiveFields
12
12
  caster: {
13
13
  class_name: "ActiveFields::Casters::IntegerArrayCaster",
14
14
  },
15
+ finder: {
16
+ class_name: "ActiveFields::Finders::IntegerArrayFinder",
17
+ },
15
18
  )
16
19
 
17
20
  store_accessor :options, :min, :max
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::TextCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::TextFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :min_length, :max_length
@@ -14,6 +14,9 @@ module ActiveFields
14
14
  caster: {
15
15
  class_name: "ActiveFields::Casters::TextArrayCaster",
16
16
  },
17
+ finder: {
18
+ class_name: "ActiveFields::Finders::TextArrayFinder",
19
+ },
17
20
  )
18
21
 
19
22
  store_accessor :options, :min_length, :max_length
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Field; end
5
+ end
@@ -15,16 +15,95 @@ module ActiveFields
15
15
  dependent: :destroy
16
16
  # rubocop:enable Rails/ReflectionClassName
17
17
 
18
+ # Searches customizables by active_values.
19
+ #
20
+ # Accepts an Array of Hashes (symbol/string keys),
21
+ # a Hash of Hashes generated from HTTP/HTML parameters
22
+ # or permitted params.
23
+ # Each element should contain:
24
+ # - <tt>:n</tt> or <tt>:name</tt> key matching the active_field record name;
25
+ # - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
26
+ # - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
27
+ #
28
+ # Example:
29
+ #
30
+ # # Array of hashes
31
+ # CustomizableModel.where_active_fields(
32
+ # [
33
+ # { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
34
+ # { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
35
+ # { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
36
+ # ],
37
+ # )
38
+ #
39
+ # # Hash of hashes generated from HTTP/HTML parameters
40
+ # CustomizableModel.where_active_fields(
41
+ # {
42
+ # "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
43
+ # "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
44
+ # "2" => { n: "boolean", op: "!=", v: false },
45
+ # },
46
+ # )
47
+ #
48
+ # # Params (must be permitted)
49
+ # CustomizableModel.where_active_fields(permitted_params)
50
+ scope :where_active_fields, ->(filters) do
51
+ filters = filters.to_h if filters.respond_to?(:permitted?)
52
+
53
+ unless filters.is_a?(Array) || filters.is_a?(Hash)
54
+ raise ArgumentError, "Hash or Array expected for `where_active_fields`, got #{filters.class.name}"
55
+ end
56
+
57
+ # Handle `fields_for` params
58
+ filters = filters.values if filters.is_a?(Hash)
59
+
60
+ active_fields_by_name = active_fields.index_by(&:name)
61
+
62
+ filters.inject(self) do |scope, filter|
63
+ filter = filter.to_h if filter.respond_to?(:permitted?)
64
+ filter = filter.with_indifferent_access
65
+
66
+ active_field = active_fields_by_name[filter[:n] || filter[:name]]
67
+ next scope if active_field.nil?
68
+ next scope if active_field.value_finder.nil?
69
+
70
+ active_values = active_field.value_finder.search(
71
+ op: filter[:op] || filter[:operator],
72
+ value: filter[:v] || filter[:value],
73
+ )
74
+ next scope if active_values.nil?
75
+
76
+ scope.where(id: active_values.select(:customizable_id))
77
+ end
78
+ end
79
+
18
80
  accepts_nested_attributes_for :active_values, allow_destroy: true
19
81
  end
20
82
 
21
- def active_fields
22
- ActiveFields.config.field_base_class.for(model_name.name)
83
+ class_methods do
84
+ # Collection of active fields registered for this customizable
85
+ def active_fields
86
+ ActiveFields.config.field_base_class.for(name)
87
+ end
88
+
89
+ # Returns active fields type names allowed for this customizable model.
90
+ def allowed_active_fields_type_names
91
+ ActiveFields.registry.field_type_names_for(name).to_a
92
+ end
93
+
94
+ # Returns active fields class names allowed for this customizable model.
95
+ def allowed_active_fields_class_names
96
+ ActiveFields.config.fields.values_at(*allowed_active_fields_type_names)
97
+ end
23
98
  end
24
99
 
100
+ delegate :active_fields, to: :class
101
+
25
102
  # Assigns the given attributes to the active_values association.
26
103
  #
27
- # Accepts an Array of Hashes (symbol/string keys) or permitted params.
104
+ # Accepts an Array of Hashes (symbol/string keys),
105
+ # a Hash of Hashes generated from HTTP/HTML parameters
106
+ # or permitted params.
28
107
  # Each element should contain a <tt>:name</tt> key matching an existing active_field record.
29
108
  # Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
30
109
  # or update an existing active_value, with the provided value.
@@ -40,6 +119,13 @@ module ActiveFields
40
119
  # { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
41
120
  # permitted_params, # params could be passed, but they must be permitted
42
121
  # ]
122
+ #
123
+ # customizable.active_fields_attributes = {
124
+ # "0" => { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
125
+ # "1" => { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
126
+ # "2" => { name: "date", _destroy: true }, # destroy (symbol keys)
127
+ # "3" => { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
128
+ # }
43
129
  def active_fields_attributes=(attributes)
44
130
  attributes = attributes.to_h if attributes.respond_to?(:permitted?)
45
131
 
@@ -26,7 +26,7 @@ module ActiveFields
26
26
  end
27
27
 
28
28
  class_methods do
29
- def acts_as_active_field(array: false, validator:, caster:)
29
+ def acts_as_active_field(array: false, validator:, caster:, finder: {})
30
30
  include FieldArrayConcern if array
31
31
 
32
32
  define_method(:array?) { array }
@@ -62,6 +62,14 @@ module ActiveFields
62
62
  end
63
63
  value_caster_class.new(**options)
64
64
  end
65
+
66
+ define_method(:value_finder_class) do
67
+ @value_finder_class ||= finder[:class_name]&.constantize
68
+ end
69
+
70
+ define_method(:value_finder) do
71
+ value_finder_class&.new(active_field: self)
72
+ end
65
73
  end
66
74
  end
67
75
 
@@ -81,6 +89,21 @@ module ActiveFields
81
89
  ActiveFields.config.fields.key(type)
82
90
  end
83
91
 
92
+ # Returns customizable types that allow this field type.
93
+ #
94
+ # Notes:
95
+ # - The customizable model must be loaded to appear in this list.
96
+ # Relationships between customizable models and field types are established in the `has_active_fields` method,
97
+ # which is typically called within the customizable model.
98
+ # If eager loading is enabled, there should be no issues.
99
+ # However, if eager loading is disabled (common in development),
100
+ # the list will remain incomplete until all customizable models are loaded.
101
+ # - Code reloading may behave incorrectly at times.
102
+ # Restart your application if you make changes to the allowed types list in `has_active_fields`.
103
+ def available_customizable_types
104
+ ActiveFields.registry.customizable_types_for(type_name).to_a
105
+ end
106
+
84
107
  private
85
108
 
86
109
  def validate_default_value
@@ -99,8 +122,8 @@ module ActiveFields
99
122
  end
100
123
 
101
124
  def validate_customizable_model_allows_type
102
- allowed_types = customizable_model&.active_fields_config&.types || []
103
- return true if allowed_types.include?(type_name)
125
+ allowed_type_names = ActiveFields.registry.field_type_names_for(customizable_type).to_a
126
+ return true if allowed_type_names.include?(type_name)
104
127
 
105
128
  errors.add(:customizable_type, :inclusion)
106
129
  false
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateActiveFieldsTables < ActiveRecord::Migration[6.0]
3
+ class CreateActiveFieldsTables < ActiveRecord::Migration[7.1]
4
4
  def change
5
5
  create_table :active_fields do |t|
6
6
  t.string :name, null: false
@@ -3,8 +3,6 @@
3
3
  module ActiveFields
4
4
  module Casters
5
5
  class DateTimeCaster < BaseCaster
6
- MAX_PRECISION = RUBY_VERSION >= "3.2" ? 9 : 6 # AR max precision is 6 for old Rubies
7
-
8
6
  def serialize(value)
9
7
  value = value.iso8601 if value.is_a?(Date)
10
8
  casted_value = caster.serialize(value)
@@ -28,7 +26,7 @@ module ActiveFields
28
26
  # Use maximum precision by default to prevent the caster from truncating useful time information
29
27
  # before precision is applied later
30
28
  def precision
31
- [options[:precision], MAX_PRECISION].compact.min
29
+ [options[:precision], ActiveFields::MAX_DATETIME_PRECISION].compact.min
32
30
  end
33
31
 
34
32
  def apply_precision(value)
@@ -15,14 +15,11 @@ module ActiveFields
15
15
  private
16
16
 
17
17
  def cast(value)
18
- casted = BigDecimal(value, 0, exception: false)
19
- casted = casted.truncate(precision) if casted && precision
20
-
21
- casted
18
+ BigDecimal(value, 0, exception: false)&.truncate(precision)
22
19
  end
23
20
 
24
21
  def precision
25
- options[:precision]
22
+ [options[:precision], ActiveFields::MAX_DECIMAL_PRECISION].compact.min
26
23
  end
27
24
  end
28
25
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ # Ruby supports up to 9 fractional seconds, but PostgreSQL, like most databases, supports only 6.
5
+ # Since we use PostgreSQL, we standardize on 6.
6
+ MAX_DATETIME_PRECISION = 6
7
+
8
+ # Ruby's BigDecimal class allows extremely high precision,
9
+ # but PostgreSQL supports a maximum of 16383 digits after the decimal point.
10
+ # Since we use PostgreSQL, we limit decimal precision to 16383.
11
+ MAX_DECIMAL_PRECISION = 2**14 - 1
12
+
13
+ OPS = {
14
+ eq: :"=",
15
+ not_eq: :"!=",
16
+ gt: :">",
17
+ gteq: :">=",
18
+ lt: :"<",
19
+ lteq: :"<=",
20
+
21
+ start_with: :"^",
22
+ end_with: :"$",
23
+ contain: :"~",
24
+ not_start_with: :"!^",
25
+ not_end_with: :"!$",
26
+ not_contain: :"!~",
27
+ istart_with: :"^*",
28
+ iend_with: :"$*",
29
+ icontain: :"~*",
30
+ not_istart_with: :"!^*",
31
+ not_iend_with: :"!$*",
32
+ not_icontain: :"!~*",
33
+
34
+ include: :"|=",
35
+ not_include: :"!|=",
36
+ any_gt: :"|>",
37
+ any_gteq: :"|>=",
38
+ any_lt: :"|<",
39
+ any_lteq: :"|<=",
40
+ all_gt: :"&>",
41
+ all_gteq: :"&>=",
42
+ all_lt: :"&<",
43
+ all_lteq: :"&<=",
44
+
45
+ size_eq: :"#=",
46
+ size_not_eq: :"#!=",
47
+ size_gt: :"#>",
48
+ size_gteq: :"#>=",
49
+ size_lt: :"#<",
50
+ size_lteq: :"#<=",
51
+
52
+ any_start_with: :"|^",
53
+ all_start_with: :"&^",
54
+ }.freeze
55
+ end
@@ -8,7 +8,8 @@ module ActiveFields
8
8
 
9
9
  # Disable models reloading to avoid STI issues.
10
10
  # Reloading can prevent subclasses from recognizing the base class.
11
- config.autoload_once_paths = %W[#{root}/app/models #{root}/app/models/concerns]
11
+ config.autoload_once_paths << "#{root}/app/models"
12
+ config.autoload_once_paths << "#{root}/app/models/concerns"
12
13
 
13
14
  initializer "active_fields.active_record" do
14
15
  ActiveSupport.on_load(:active_record) do
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Finders
5
+ class ArrayFinder < BaseFinder
6
+ private
7
+
8
+ # This method defines a JSONPath query for searching within a `jsonb` field.
9
+ # It must be implemented in a subclass and include a `$value` parameter,
10
+ # which will be replaced with the provided value during search operations.
11
+ # Refer to descendant implementations for examples.
12
+ # For more details, see the PostgreSQL documentation: https://www.postgresql.org/docs/current/functions-json.html
13
+ def jsonpath(operator) = raise NotImplementedError
14
+
15
+ # Arel query node that searches for records where any element matches the JSONPath
16
+ def value_match_any(operator, value)
17
+ jsonb_path_exists(
18
+ value_field_jsonb,
19
+ jsonpath(operator),
20
+ { value: value },
21
+ )
22
+ end
23
+
24
+ # Arel query node that searches for records where all elements match the JSONPath
25
+ def value_match_all(operator, value)
26
+ jsonb_array_length(value_field_jsonb).gt(0)
27
+ .and(
28
+ jsonb_array_length(value_field_jsonb).eq(
29
+ jsonb_array_length(
30
+ jsonb_path_query_array(
31
+ value_field_jsonb,
32
+ jsonpath(operator),
33
+ { value: value },
34
+ ),
35
+ ),
36
+ ),
37
+ )
38
+ end
39
+
40
+ # Arel query node that searches for records where the array size is equal to the provided value
41
+ def value_size_eq(value)
42
+ value = cast_int(value)
43
+ jsonb_array_length(value_field_jsonb).eq(value)
44
+ end
45
+
46
+ # Arel query node that searches for records where the array size is not equal to the provided value
47
+ def value_size_not_eq(value)
48
+ value = cast_int(value)
49
+ jsonb_array_length(value_field_jsonb).not_eq(value)
50
+ end
51
+
52
+ # Arel query node that searches for records where the array size is greater than the provided value
53
+ def value_size_gt(value)
54
+ value = cast_int(value)
55
+ jsonb_array_length(value_field_jsonb).gt(value)
56
+ end
57
+
58
+ # Arel query node that searches for records where the array size is greater than or equal to the provided value
59
+ def value_size_gteq(value)
60
+ value = cast_int(value)
61
+ jsonb_array_length(value_field_jsonb).gteq(value)
62
+ end
63
+
64
+ # Arel query node that searches for records where the array size is less than the provided value
65
+ def value_size_lt(value)
66
+ value = cast_int(value)
67
+ jsonb_array_length(value_field_jsonb).lt(value)
68
+ end
69
+
70
+ # Arel query node that searches for records where the array size is less than or equal to the provided value
71
+ def value_size_lteq(value)
72
+ value = cast_int(value)
73
+ jsonb_array_length(value_field_jsonb).lteq(value)
74
+ end
75
+
76
+ # Casts given value to integer, useful for querying by array size
77
+ def cast_int(value)
78
+ Casters::IntegerCaster.new.deserialize(value)
79
+ end
80
+
81
+ # Arel node for `active_fields.value_meta->const`
82
+ def value_field_jsonb
83
+ Arel::Nodes::InfixOperation.new(
84
+ "->",
85
+ Arel::Table.new(cte_name)[:value_meta],
86
+ Arel::Nodes.build_quoted("const"),
87
+ )
88
+ end
89
+
90
+ # Arel node for `jsonb_path_exists` PostgreSQL function
91
+ def jsonb_path_exists(target, jsonpath, vars = nil)
92
+ Arel::Nodes::NamedFunction.new(
93
+ "jsonb_path_exists",
94
+ [target, *[jsonpath, vars&.to_json].compact.map { Arel::Nodes.build_quoted(_1) }],
95
+ )
96
+ end
97
+
98
+ # Arel node for `jsonb_path_query_array` PostgreSQL function
99
+ def jsonb_path_query_array(target, jsonpath, vars = nil)
100
+ Arel::Nodes::NamedFunction.new(
101
+ "jsonb_path_query_array",
102
+ [target, *[jsonpath, vars&.to_json].compact.map { Arel::Nodes.build_quoted(_1) }],
103
+ )
104
+ end
105
+
106
+ # Arel node for `jsonb_array_length` PostgreSQL function
107
+ def jsonb_array_length(target)
108
+ Arel::Nodes::NamedFunction.new("jsonb_array_length", [target])
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Finders
5
+ class BaseFinder
6
+ class << self
7
+ # Define search operation
8
+ def operation(name, operator:, &block)
9
+ name = name.to_sym
10
+ operator = operator.to_sym
11
+
12
+ __operations__[name] = {
13
+ operator: operator.to_sym,
14
+ block: block,
15
+ }
16
+ __operators__[operator] = name
17
+ end
18
+
19
+ # Returns operator for provided search operation name
20
+ def operator_for(operation_name)
21
+ __operations__.dig(operation_name.to_sym, :operator)
22
+ end
23
+
24
+ # Returns search operation name for provided operator
25
+ def operation_for(operator)
26
+ __operators__[operator.to_sym]
27
+ end
28
+
29
+ # Returns all defined search operations names
30
+ def operations
31
+ __operations__.keys
32
+ end
33
+
34
+ # Storage for defined operations. Private.
35
+ def __operations__
36
+ @__operations__ ||= {}
37
+ end
38
+
39
+ # Index for finding operation by operator. Private.
40
+ def __operators__
41
+ @__operators__ ||= {}
42
+ end
43
+ end
44
+
45
+ attr_reader :active_field
46
+
47
+ def initialize(active_field:)
48
+ @active_field = active_field
49
+ end
50
+
51
+ # Perform query operation
52
+ # @param op [String, Symbol] Operation name or operator
53
+ # @param value [Any] The value to search for
54
+ def search(op:, value:)
55
+ op = op.to_sym
56
+ operation = self.class.__operations__.key?(op) ? op : self.class.__operators__[op]
57
+ return if operation.nil?
58
+
59
+ instance_exec(value, &self.class.__operations__[operation][:block])
60
+ end
61
+
62
+ private
63
+
64
+ # Name of the CTE, that is used in queries. It is the original values table name.
65
+ def cte_name = ActiveFields.config.value_class.table_name
66
+
67
+ # Base scope for querying
68
+ def scope
69
+ ActiveFields.config.value_class.with(cte_name => active_field.active_values)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Finders
5
+ class BooleanFinder < SingularFinder
6
+ operation :eq, operator: OPS[:eq] do |value|
7
+ scope.where(eq(casted_value_field("boolean"), cast(value)))
8
+ end
9
+ operation :not_eq, operator: OPS[:not_eq] do |value|
10
+ scope.where(not_eq(casted_value_field("boolean"), cast(value)))
11
+ end
12
+
13
+ private
14
+
15
+ def cast(value)
16
+ Casters::BooleanCaster.new.deserialize(value)
17
+ end
18
+ end
19
+ end
20
+ end