active_fields 1.0.0 → 2.0.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +33 -2
  4. data/README.md +478 -90
  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 +93 -4
  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 +7 -0
  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 +79 -31
  46. data/lib/generators/active_fields/install/install_generator.rb +1 -1
  47. data/lib/generators/active_fields/scaffold/USAGE +9 -0
  48. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +34 -0
  49. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +133 -0
  50. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  51. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +100 -0
  52. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  53. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/array_field_controller.js +25 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/edit.html.erb +5 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +53 -0
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +58 -0
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +70 -0
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +63 -0
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +75 -0
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +63 -0
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +76 -0
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +61 -0
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +73 -0
  79. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +58 -0
  80. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +70 -0
  81. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +53 -0
  82. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +70 -0
  83. data/lib/generators/active_fields/scaffold/templates/views/active_fields/index.html.erb +41 -0
  84. data/lib/generators/active_fields/scaffold/templates/views/active_fields/new.html.erb +5 -0
  85. data/lib/generators/active_fields/scaffold/templates/views/active_fields/show.html.erb +29 -0
  86. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_boolean.html.erb +8 -0
  87. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date.html.erb +4 -0
  88. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date_array.html.erb +12 -0
  89. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime.html.erb +4 -0
  90. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime_array.html.erb +12 -0
  91. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal.html.erb +4 -0
  92. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal_array.html.erb +12 -0
  93. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum.html.erb +4 -0
  94. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum_array.html.erb +4 -0
  95. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer.html.erb +4 -0
  96. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer_array.html.erb +12 -0
  97. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text.html.erb +4 -0
  98. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text_array.html.erb +12 -0
  99. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +19 -0
  100. metadata +78 -10
  101. data/lib/active_fields/customizable_config.rb +0 -24
@@ -11,6 +11,9 @@ module ActiveFields
11
11
  caster: {
12
12
  class_name: "ActiveFields::Casters::BooleanCaster",
13
13
  },
14
+ finder: {
15
+ class_name: "ActiveFields::Finders::BooleanFinder",
16
+ },
14
17
  )
15
18
 
16
19
  store_accessor :options, :required, :nullable
@@ -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
 
@@ -80,7 +166,8 @@ module ActiveFields
80
166
 
81
167
  alias_method :active_fields=, :active_fields_attributes=
82
168
 
83
- # Build an active_value, if it doesn't exist, with a default value for each available active_field
169
+ # Build an active_value, if it doesn't exist, with a default value for each available active_field.
170
+ # Returns active_values collection.
84
171
  def initialize_active_values
85
172
  existing_field_ids = active_values.map(&:active_field_id)
86
173
 
@@ -89,6 +176,8 @@ module ActiveFields
89
176
 
90
177
  active_values.new(active_field: active_field, value: active_field.default_value)
91
178
  end
179
+
180
+ active_values
92
181
  end
93
182
  end
94
183
  end
@@ -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
@@ -4,6 +4,13 @@ module ActiveFields
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace ActiveFields
6
6
 
7
+ config.eager_load_namespaces << ActiveFields
8
+
9
+ # Disable models reloading to avoid STI issues.
10
+ # Reloading can prevent subclasses from recognizing the base class.
11
+ config.autoload_once_paths << "#{root}/app/models"
12
+ config.autoload_once_paths << "#{root}/app/models/concerns"
13
+
7
14
  initializer "active_fields.active_record" do
8
15
  ActiveSupport.on_load(:active_record) do
9
16
  include HasActiveFields
@@ -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