active_fields 1.1.0 → 2.0.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +32 -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 -5
  20. data/app/models/concerns/active_fields/field_concern.rb +26 -5
  21. data/app/models/concerns/active_fields/value_concern.rb +0 -2
  22. data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
  23. data/lib/active_fields/casters/date_time_caster.rb +1 -3
  24. data/lib/active_fields/casters/decimal_caster.rb +2 -5
  25. data/lib/active_fields/constants.rb +55 -0
  26. data/lib/active_fields/engine.rb +2 -1
  27. data/lib/active_fields/finders/array_finder.rb +112 -0
  28. data/lib/active_fields/finders/base_finder.rb +73 -0
  29. data/lib/active_fields/finders/boolean_finder.rb +20 -0
  30. data/lib/active_fields/finders/date_array_finder.rb +65 -0
  31. data/lib/active_fields/finders/date_finder.rb +32 -0
  32. data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
  33. data/lib/active_fields/finders/date_time_finder.rb +32 -0
  34. data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
  35. data/lib/active_fields/finders/decimal_finder.rb +32 -0
  36. data/lib/active_fields/finders/enum_array_finder.rb +41 -0
  37. data/lib/active_fields/finders/enum_finder.rb +20 -0
  38. data/lib/active_fields/finders/integer_array_finder.rb +65 -0
  39. data/lib/active_fields/finders/integer_finder.rb +32 -0
  40. data/lib/active_fields/finders/singular_finder.rb +66 -0
  41. data/lib/active_fields/finders/text_array_finder.rb +47 -0
  42. data/lib/active_fields/finders/text_finder.rb +81 -0
  43. data/lib/active_fields/has_active_fields.rb +3 -4
  44. data/lib/active_fields/registry.rb +38 -0
  45. data/lib/active_fields/version.rb +1 -1
  46. data/lib/active_fields.rb +29 -1
  47. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +9 -0
  48. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +0 -10
  49. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  50. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +67 -0
  51. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  52. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  53. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +1 -1
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +1 -1
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +1 -1
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +2 -2
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +2 -2
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +2 -2
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +2 -2
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +1 -1
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +1 -1
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +1 -1
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +1 -1
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +1 -1
  79. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +1 -1
  80. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +1 -1
  81. metadata +42 -10
  82. 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
@@ -6,25 +6,102 @@ module ActiveFields
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- # rubocop:disable Rails/ReflectionClassName
10
9
  has_many :active_values,
11
10
  class_name: ActiveFields.config.value_class_name,
12
11
  as: :customizable,
13
12
  inverse_of: :customizable,
14
13
  autosave: true,
15
14
  dependent: :destroy
16
- # rubocop:enable Rails/ReflectionClassName
15
+
16
+ # Searches customizables by active_values.
17
+ #
18
+ # Accepts an Array of Hashes (symbol/string keys),
19
+ # a Hash of Hashes generated from HTTP/HTML parameters
20
+ # or permitted params.
21
+ # Each element should contain:
22
+ # - <tt>:n</tt> or <tt>:name</tt> key matching the active_field record name;
23
+ # - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
24
+ # - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
25
+ #
26
+ # Example:
27
+ #
28
+ # # Array of hashes
29
+ # CustomizableModel.where_active_fields(
30
+ # [
31
+ # { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
32
+ # { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
33
+ # { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
34
+ # ],
35
+ # )
36
+ #
37
+ # # Hash of hashes generated from HTTP/HTML parameters
38
+ # CustomizableModel.where_active_fields(
39
+ # {
40
+ # "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
41
+ # "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
42
+ # "2" => { n: "boolean", op: "!=", v: false },
43
+ # },
44
+ # )
45
+ #
46
+ # # Params (must be permitted)
47
+ # CustomizableModel.where_active_fields(permitted_params)
48
+ scope :where_active_fields, ->(filters) do
49
+ filters = filters.to_h if filters.respond_to?(:permitted?)
50
+
51
+ unless filters.is_a?(Array) || filters.is_a?(Hash)
52
+ raise ArgumentError, "Hash or Array expected for `where_active_fields`, got #{filters.class.name}"
53
+ end
54
+
55
+ # Handle `fields_for` params
56
+ filters = filters.values if filters.is_a?(Hash)
57
+
58
+ active_fields_by_name = active_fields.index_by(&:name)
59
+
60
+ filters.inject(self) do |scope, filter|
61
+ filter = filter.to_h if filter.respond_to?(:permitted?)
62
+ filter = filter.with_indifferent_access
63
+
64
+ active_field = active_fields_by_name[filter[:n] || filter[:name]]
65
+ next scope if active_field.nil?
66
+ next scope if active_field.value_finder.nil?
67
+
68
+ active_values = active_field.value_finder.search(
69
+ op: filter[:op] || filter[:operator],
70
+ value: filter[:v] || filter[:value],
71
+ )
72
+ next scope if active_values.nil?
73
+
74
+ scope.where(id: active_values.select(:customizable_id))
75
+ end
76
+ end
17
77
 
18
78
  accepts_nested_attributes_for :active_values, allow_destroy: true
19
79
  end
20
80
 
21
- def active_fields
22
- ActiveFields.config.field_base_class.for(model_name.name)
81
+ class_methods do
82
+ # Collection of active fields registered for this customizable
83
+ def active_fields
84
+ ActiveFields.config.field_base_class.for(name)
85
+ end
86
+
87
+ # Returns active fields type names allowed for this customizable model.
88
+ def allowed_active_fields_type_names
89
+ ActiveFields.registry.field_type_names_for(name).to_a
90
+ end
91
+
92
+ # Returns active fields class names allowed for this customizable model.
93
+ def allowed_active_fields_class_names
94
+ ActiveFields.config.fields.values_at(*allowed_active_fields_type_names)
95
+ end
23
96
  end
24
97
 
98
+ delegate :active_fields, to: :class
99
+
25
100
  # Assigns the given attributes to the active_values association.
26
101
  #
27
- # Accepts an Array of Hashes (symbol/string keys) or permitted params.
102
+ # Accepts an Array of Hashes (symbol/string keys),
103
+ # a Hash of Hashes generated from HTTP/HTML parameters
104
+ # or permitted params.
28
105
  # Each element should contain a <tt>:name</tt> key matching an existing active_field record.
29
106
  # Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
30
107
  # or update an existing active_value, with the provided value.
@@ -40,6 +117,13 @@ module ActiveFields
40
117
  # { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
41
118
  # permitted_params, # params could be passed, but they must be permitted
42
119
  # ]
120
+ #
121
+ # customizable.active_fields_attributes = {
122
+ # "0" => { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
123
+ # "1" => { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
124
+ # "2" => { name: "date", _destroy: true }, # destroy (symbol keys)
125
+ # "3" => { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
126
+ # }
43
127
  def active_fields_attributes=(attributes)
44
128
  attributes = attributes.to_h if attributes.respond_to?(:permitted?)
45
129
 
@@ -6,13 +6,11 @@ module ActiveFields
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- # rubocop:disable Rails/ReflectionClassName
10
9
  has_many :active_values,
11
10
  class_name: ActiveFields.config.value_class_name,
12
11
  foreign_key: :active_field_id,
13
12
  inverse_of: :active_field,
14
13
  dependent: :destroy
15
- # rubocop:enable Rails/ReflectionClassName
16
14
 
17
15
  scope :for, ->(customizable_type) { where(customizable_type: customizable_type) }
18
16
 
@@ -26,7 +24,7 @@ module ActiveFields
26
24
  end
27
25
 
28
26
  class_methods do
29
- def acts_as_active_field(array: false, validator:, caster:)
27
+ def acts_as_active_field(array: false, validator:, caster:, finder: {})
30
28
  include FieldArrayConcern if array
31
29
 
32
30
  define_method(:array?) { array }
@@ -62,6 +60,14 @@ module ActiveFields
62
60
  end
63
61
  value_caster_class.new(**options)
64
62
  end
63
+
64
+ define_method(:value_finder_class) do
65
+ @value_finder_class ||= finder[:class_name]&.constantize
66
+ end
67
+
68
+ define_method(:value_finder) do
69
+ value_finder_class&.new(active_field: self)
70
+ end
65
71
  end
66
72
  end
67
73
 
@@ -81,6 +87,21 @@ module ActiveFields
81
87
  ActiveFields.config.fields.key(type)
82
88
  end
83
89
 
90
+ # Returns customizable types that allow this field type.
91
+ #
92
+ # Notes:
93
+ # - The customizable model must be loaded to appear in this list.
94
+ # Relationships between customizable models and field types are established in the `has_active_fields` method,
95
+ # which is typically called within the customizable model.
96
+ # If eager loading is enabled, there should be no issues.
97
+ # However, if eager loading is disabled (common in development),
98
+ # the list will remain incomplete until all customizable models are loaded.
99
+ # - Code reloading may behave incorrectly at times.
100
+ # Restart your application if you make changes to the allowed types list in `has_active_fields`.
101
+ def available_customizable_types
102
+ ActiveFields.registry.customizable_types_for(type_name).to_a
103
+ end
104
+
84
105
  private
85
106
 
86
107
  def validate_default_value
@@ -99,8 +120,8 @@ module ActiveFields
99
120
  end
100
121
 
101
122
  def validate_customizable_model_allows_type
102
- allowed_types = customizable_model&.active_fields_config&.types || []
103
- return true if allowed_types.include?(type_name)
123
+ allowed_type_names = ActiveFields.registry.field_type_names_for(customizable_type).to_a
124
+ return true if allowed_type_names.include?(type_name)
104
125
 
105
126
  errors.add(:customizable_type, :inclusion)
106
127
  false
@@ -7,12 +7,10 @@ module ActiveFields
7
7
 
8
8
  included do
9
9
  belongs_to :customizable, polymorphic: true, optional: false, inverse_of: :active_values
10
- # rubocop:disable Rails/ReflectionClassName
11
10
  belongs_to :active_field,
12
11
  class_name: ActiveFields.config.field_base_class_name,
13
12
  optional: false,
14
13
  inverse_of: :active_values
15
- # rubocop:enable Rails/ReflectionClassName
16
14
 
17
15
  validates :active_field, uniqueness: { scope: :customizable }
18
16
  validate :validate_value
@@ -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