active_fields 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/README.md +633 -0
  4. data/app/models/active_fields/field/boolean.rb +10 -3
  5. data/app/models/active_fields/field/date.rb +10 -4
  6. data/app/models/active_fields/field/date_array.rb +12 -18
  7. data/app/models/active_fields/field/date_time.rb +77 -0
  8. data/app/models/active_fields/field/date_time_array.rb +61 -0
  9. data/app/models/active_fields/field/decimal.rb +35 -6
  10. data/app/models/active_fields/field/decimal_array.rb +28 -11
  11. data/app/models/active_fields/field/enum.rb +28 -9
  12. data/app/models/active_fields/field/enum_array.rb +29 -22
  13. data/app/models/active_fields/field/integer.rb +10 -4
  14. data/app/models/active_fields/field/integer_array.rb +12 -8
  15. data/app/models/active_fields/field/text.rb +10 -4
  16. data/app/models/active_fields/field/text_array.rb +14 -8
  17. data/app/models/concerns/active_fields/customizable_concern.rb +60 -51
  18. data/app/models/concerns/active_fields/field_array_concern.rb +25 -0
  19. data/app/models/concerns/active_fields/field_concern.rb +45 -24
  20. data/app/models/concerns/active_fields/value_concern.rb +32 -4
  21. data/db/migrate/20240229230000_create_active_fields_tables.rb +2 -2
  22. data/lib/active_fields/casters/base_caster.rb +8 -2
  23. data/lib/active_fields/casters/date_caster.rb +8 -6
  24. data/lib/active_fields/casters/date_time_array_caster.rb +19 -0
  25. data/lib/active_fields/casters/date_time_caster.rb +43 -0
  26. data/lib/active_fields/casters/decimal_caster.rb +10 -2
  27. data/lib/active_fields/casters/enum_array_caster.rb +13 -1
  28. data/lib/active_fields/casters/enum_caster.rb +15 -1
  29. data/lib/active_fields/casters/integer_caster.rb +2 -2
  30. data/lib/active_fields/config.rb +12 -1
  31. data/lib/active_fields/customizable_config.rb +1 -1
  32. data/lib/active_fields/has_active_fields.rb +1 -1
  33. data/lib/active_fields/validators/base_validator.rb +7 -3
  34. data/lib/active_fields/validators/boolean_validator.rb +2 -2
  35. data/lib/active_fields/validators/date_array_validator.rb +3 -3
  36. data/lib/active_fields/validators/date_time_array_validator.rb +26 -0
  37. data/lib/active_fields/validators/date_time_validator.rb +19 -0
  38. data/lib/active_fields/validators/date_validator.rb +3 -3
  39. data/lib/active_fields/validators/decimal_array_validator.rb +2 -2
  40. data/lib/active_fields/validators/decimal_validator.rb +2 -2
  41. data/lib/active_fields/validators/enum_array_validator.rb +3 -3
  42. data/lib/active_fields/validators/enum_validator.rb +3 -3
  43. data/lib/active_fields/validators/integer_array_validator.rb +2 -2
  44. data/lib/active_fields/validators/integer_validator.rb +2 -2
  45. data/lib/active_fields/validators/text_array_validator.rb +2 -2
  46. data/lib/active_fields/validators/text_validator.rb +2 -2
  47. data/lib/active_fields/version.rb +1 -1
  48. data/lib/active_fields.rb +4 -0
  49. data/lib/generators/active_fields/install/USAGE +5 -0
  50. data/lib/generators/active_fields/install/install_generator.rb +29 -0
  51. metadata +11 -16
@@ -1,85 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFields
4
- # Mix-in that adds the active fields functionality to the ActiveRecord model
4
+ # Model mix-in that adds the active fields functionality to the customizable model
5
5
  module CustomizableConcern
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- # active_values association for the owner record.
10
- # We skip built-in autosave because it doesn't work if the owner record isn't changed.
11
- # Instead, we implement our own autosave callback: `save_changed_active_values`.
12
9
  # rubocop:disable Rails/ReflectionClassName
13
10
  has_many :active_values,
14
11
  class_name: ActiveFields.config.value_class_name,
15
12
  as: :customizable,
16
13
  inverse_of: :customizable,
17
- autosave: false,
14
+ autosave: true,
18
15
  dependent: :destroy
19
16
  # rubocop:enable Rails/ReflectionClassName
20
17
 
21
- # Firstly, we build active_values that hasn't been already created.
22
- # Than, we set values for active_values whose values should be changed
23
- # according to `active_values_attributes`.
24
- before_validation :initialize_active_values
25
-
26
- # Save all changed active_values on the owner record save.
27
- after_save :save_changed_active_values
28
-
29
- # We always validate active_values on the owner record change,
30
- # as if they are just an ordinary record columns.
31
- validates_associated :active_values
32
-
33
- # This virtual attribute is used for setting active_values values.
34
- # Keys are active_fields names,
35
- # values are values for corresponding active_values of the record.
36
- attr_reader :active_values_attributes
18
+ accepts_nested_attributes_for :active_values, allow_destroy: true
37
19
  end
38
20
 
39
21
  def active_fields
40
22
  ActiveFields.config.field_base_class.for(model_name.name)
41
23
  end
42
24
 
43
- # Convert hash keys to strings for easier access by fields names.
44
- def active_values_attributes=(value)
45
- @active_values_attributes =
46
- if value.respond_to?(:to_h)
47
- value.to_h.transform_keys(&:to_s)
48
- else
49
- value
50
- end
51
- end
25
+ # Assigns the given attributes to the active_values association.
26
+ #
27
+ # Accepts an Array of Hashes (symbol/string keys) or permitted params.
28
+ # Each element should contain a <tt>:name</tt> key matching an existing active_field record.
29
+ # Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
30
+ # or update an existing active_value, with the provided value.
31
+ # Element with a <tt>:_destroy</tt> key set to a truthy value will mark the
32
+ # matched active_value for destruction.
33
+ #
34
+ # Example:
35
+ #
36
+ # customizable.active_fields_attributes = [
37
+ # { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
38
+ # { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
39
+ # { name: "date", _destroy: true }, # destroy (symbol keys)
40
+ # { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
41
+ # permitted_params, # params could be passed, but they must be permitted
42
+ # ]
43
+ def active_fields_attributes=(attributes)
44
+ attributes = attributes.to_h if attributes.respond_to?(:permitted?)
45
+
46
+ unless attributes.is_a?(Array) || attributes.is_a?(Hash)
47
+ raise ArgumentError, "Array or Hash expected for `active_fields=`, got #{attributes.class.name}"
48
+ end
52
49
 
53
- private
50
+ # Handle `fields_for` params
51
+ attributes = attributes.values if attributes.is_a?(Hash)
54
52
 
55
- def initialize_active_values
56
- active_fields.each do |active_field|
57
- active_value =
58
- find_active_value_by_field(active_field) ||
59
- active_values.new(active_field: active_field, value: active_field.default_value)
53
+ active_fields_by_name = active_fields.index_by(&:name)
54
+ active_values_by_field_id = active_values.index_by(&:active_field_id)
55
+
56
+ nested_attributes = attributes.filter_map do |active_value_attributes|
57
+ # Convert params to Hash
58
+ active_value_attributes = active_value_attributes.to_h if active_value_attributes.respond_to?(:permitted?)
59
+ active_value_attributes = active_value_attributes.with_indifferent_access
60
60
 
61
- next unless active_values_attributes.is_a?(Hash)
62
- next unless active_values_attributes.key?(active_field.name)
61
+ active_field = active_fields_by_name[active_value_attributes[:name]]
62
+ next if active_field.nil?
63
63
 
64
- active_value.value = active_values_attributes[active_field.name]
64
+ active_value = active_values_by_field_id[active_field.id]
65
+
66
+ if has_destroy_flag?(active_value_attributes)
67
+ # Destroy
68
+ { id: active_value&.id, _destroy: true }
69
+ elsif active_value
70
+ # Update
71
+ { id: active_value.id, value: active_value_attributes[:value] }
72
+ else
73
+ # Create
74
+ { active_field: active_field, value: active_value_attributes[:value] }
75
+ end
65
76
  end
77
+
78
+ self.active_values_attributes = nested_attributes
66
79
  end
67
80
 
68
- def save_changed_active_values
69
- active_values.each do |active_value|
70
- next unless active_value.new_record? || active_value.changed?
81
+ alias_method :active_fields=, :active_fields_attributes=
71
82
 
72
- # For new records association id isn't set right, so we force reassignment of the customizable
73
- active_value.customizable = self
83
+ # Build an active_value, if it doesn't exist, with a default value for each available active_field
84
+ def initialize_active_values
85
+ existing_field_ids = active_values.map(&:active_field_id)
74
86
 
75
- # Do not validate active values twice,
76
- # because they have already been validated by `validates_associated`.
77
- active_value.save!(validate: false)
78
- end
79
- end
87
+ active_fields.each do |active_field|
88
+ next if existing_field_ids.include?(active_field.id)
80
89
 
81
- def find_active_value_by_field(active_field)
82
- active_values.find { |active_value| active_value.active_field_id == active_field.id }
90
+ active_values.new(active_field: active_field, value: active_field.default_value)
91
+ end
83
92
  end
84
93
  end
85
94
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ # Model mix-in that adds array functionality to the active fields model
5
+ module FieldArrayConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ store_accessor :options, :min_size, :max_size
10
+
11
+ validates :min_size, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true
12
+ validates :max_size, comparison: { greater_than_or_equal_to: ->(r) { r.min_size || 0 } }, allow_nil: true
13
+
14
+ %i[min_size max_size].each do |column|
15
+ define_method(column) do
16
+ Casters::IntegerCaster.new.deserialize(super())
17
+ end
18
+
19
+ define_method(:"#{column}=") do |other|
20
+ super(Casters::IntegerCaster.new.serialize(other))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFields
4
- # Mix-in with a base logic for the active fields model
4
+ # Model mix-in with a base logic for the active fields model
5
5
  module FieldConcern
6
6
  extend ActiveSupport::Concern
7
7
 
@@ -22,36 +22,63 @@ module ActiveFields
22
22
  validate :validate_default_value
23
23
  validate :validate_customizable_model_allows_type
24
24
 
25
- after_create :add_field_to_records
26
25
  after_initialize :set_defaults
27
26
  end
28
27
 
29
- def value_validator_class
30
- @value_validator_class ||= "ActiveFields::Validators::#{model_name.name.demodulize}Validator".constantize
31
- end
28
+ class_methods do
29
+ def acts_as_active_field(array: false, validator:, caster:)
30
+ include FieldArrayConcern if array
32
31
 
33
- def value_validator
34
- value_validator_class.new(self)
35
- end
32
+ define_method(:array?) { array }
36
33
 
37
- def value_caster_class
38
- @value_caster_class ||= "ActiveFields::Casters::#{model_name.name.demodulize}Caster".constantize
39
- end
34
+ define_method(:value_validator_class) do
35
+ @value_validator_class ||= validator[:class_name].constantize
36
+ end
37
+
38
+ define_method(:value_validator) do
39
+ options =
40
+ if validator[:options].nil?
41
+ {}
42
+ elsif validator[:options].arity == 0
43
+ instance_exec(&validator[:options])
44
+ else
45
+ validator[:options].call(self)
46
+ end
47
+ value_validator_class.new(**options)
48
+ end
49
+
50
+ define_method(:value_caster_class) do
51
+ @value_caster_class ||= caster[:class_name].constantize
52
+ end
40
53
 
41
- def value_caster
42
- value_caster_class.new
54
+ define_method(:value_caster) do
55
+ options =
56
+ if caster[:options].nil?
57
+ {}
58
+ elsif caster[:options].arity == 0
59
+ instance_exec(&caster[:options])
60
+ else
61
+ caster[:options].call(self)
62
+ end
63
+ value_caster_class.new(**options)
64
+ end
65
+ end
43
66
  end
44
67
 
45
68
  def customizable_model
46
- customizable_type.constantize
69
+ customizable_type.safe_constantize
47
70
  end
48
71
 
49
72
  def default_value=(v)
50
- super(value_caster.serialize(v))
73
+ default_value_meta["const"] = value_caster.serialize(v)
51
74
  end
52
75
 
53
76
  def default_value
54
- value_caster.deserialize(super)
77
+ value_caster.deserialize(default_value_meta["const"])
78
+ end
79
+
80
+ def type_name
81
+ ActiveFields.config.fields.key(type)
55
82
  end
56
83
 
57
84
  private
@@ -72,19 +99,13 @@ module ActiveFields
72
99
  end
73
100
 
74
101
  def validate_customizable_model_allows_type
75
- allowed_types = customizable_model.active_fields_config&.types || []
76
- return true if ActiveFields.config.fields.values_at(*allowed_types).include?(type)
102
+ allowed_types = customizable_model&.active_fields_config&.types || []
103
+ return true if allowed_types.include?(type_name)
77
104
 
78
105
  errors.add(:customizable_type, :inclusion)
79
106
  false
80
107
  end
81
108
 
82
- def add_field_to_records
83
- customizable_model.find_each do |record|
84
- ActiveFields.config.value_class.create!(active_field: self, customizable: record, value: default_value)
85
- end
86
- end
87
-
88
109
  def set_defaults; end
89
110
  end
90
111
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFields
4
- # Mix-in with a base logic for the active fields value model
4
+ # Model mix-in with a base logic for the active fields value model
5
5
  module ValueConcern
6
6
  extend ActiveSupport::Concern
7
7
 
@@ -14,17 +14,32 @@ module ActiveFields
14
14
  inverse_of: :active_values
15
15
  # rubocop:enable Rails/ReflectionClassName
16
16
 
17
- validates :active_field_id, uniqueness: { scope: %i[customizable_id customizable_type] }
17
+ validates :active_field, uniqueness: { scope: :customizable }
18
18
  validate :validate_value
19
19
  validate :validate_customizable_allowed
20
+
21
+ before_validation :assign_value_from_temp, if: -> { temp_value && active_field }
20
22
  end
21
23
 
24
+ delegate :name, to: :active_field, allow_nil: true
25
+
26
+ attr_reader :temp_value
27
+
22
28
  def value=(v)
23
- super(active_field&.value_caster&.serialize(v))
29
+ if active_field
30
+ clear_temp_value
31
+ value_meta["const"] = active_field.value_caster.serialize(v)
32
+ else
33
+ assign_temp_value(v)
34
+ end
24
35
  end
25
36
 
26
37
  def value
27
- active_field&.value_caster&.deserialize(super)
38
+ return unless active_field
39
+
40
+ assign_value_from_temp if temp_value
41
+
42
+ active_field.value_caster.deserialize(value_meta["const"])
28
43
  end
29
44
 
30
45
  private
@@ -51,5 +66,18 @@ module ActiveFields
51
66
  errors.add(:customizable, :invalid)
52
67
  false
53
68
  end
69
+
70
+ # Wrap the provided value to differentiate between explicitly setting it to nil and not setting it at all
71
+ def assign_temp_value(v)
72
+ @temp_value = { "value" => v }
73
+ end
74
+
75
+ def clear_temp_value
76
+ @temp_value = nil
77
+ end
78
+
79
+ def assign_value_from_temp
80
+ self.value = temp_value["value"]
81
+ end
54
82
  end
55
83
  end
@@ -6,7 +6,7 @@ class CreateActiveFieldsTables < ActiveRecord::Migration[6.0]
6
6
  t.string :name, null: false
7
7
  t.string :type, null: false
8
8
  t.string :customizable_type, null: false
9
- t.jsonb :default_value
9
+ t.jsonb :default_value_meta, null: false, default: {}
10
10
  t.jsonb :options, null: false, default: {}
11
11
 
12
12
  t.timestamps
@@ -20,7 +20,7 @@ class CreateActiveFieldsTables < ActiveRecord::Migration[6.0]
20
20
  t.references :active_field,
21
21
  null: false,
22
22
  foreign_key: { to_table: :active_fields, name: "active_fields_values_active_field_id_fk" }
23
- t.jsonb :value
23
+ t.jsonb :value_meta, null: false, default: {}
24
24
 
25
25
  t.timestamps
26
26
 
@@ -4,12 +4,18 @@ module ActiveFields
4
4
  module Casters
5
5
  # Typecasts the active_value value
6
6
  class BaseCaster
7
- # To raw DB value
7
+ attr_reader :options
8
+
9
+ def initialize(**options)
10
+ @options = options
11
+ end
12
+
13
+ # To raw AR attribute value
8
14
  def serialize(value)
9
15
  raise NotImplementedError
10
16
  end
11
17
 
12
- # From raw DB value
18
+ # From raw AR attribute value
13
19
  def deserialize(value)
14
20
  raise NotImplementedError
15
21
  end
@@ -4,19 +4,21 @@ module ActiveFields
4
4
  module Casters
5
5
  class DateCaster < BaseCaster
6
6
  def serialize(value)
7
- cast(value)&.iso8601
7
+ casted_value = caster.serialize(value)
8
+
9
+ casted_value.iso8601 if casted_value.is_a?(Date)
8
10
  end
9
11
 
10
12
  def deserialize(value)
11
- cast(value)
13
+ casted_value = caster.deserialize(value)
14
+
15
+ casted_value if casted_value.is_a?(Date)
12
16
  end
13
17
 
14
18
  private
15
19
 
16
- def cast(value)
17
- casted_value = ActiveModel::Type::Date.new.cast(value)
18
-
19
- casted_value if casted_value.acts_like?(:date)
20
+ def caster
21
+ ActiveRecord::Type::Date.new
20
22
  end
21
23
  end
22
24
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Casters
5
+ class DateTimeArrayCaster < DateTimeCaster
6
+ def serialize(value)
7
+ return unless value.is_a?(Array)
8
+
9
+ value.map { super(_1) }
10
+ end
11
+
12
+ def deserialize(value)
13
+ return unless value.is_a?(Array)
14
+
15
+ value.map { super(_1) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFields
4
+ module Casters
5
+ class DateTimeCaster < BaseCaster
6
+ MAX_PRECISION = RUBY_VERSION >= "3.2" ? 9 : 6 # AR max precision is 6 for old Rubies
7
+
8
+ def serialize(value)
9
+ value = value.iso8601 if value.is_a?(Date)
10
+ casted_value = caster.serialize(value)
11
+
12
+ casted_value.iso8601(precision) if casted_value.acts_like?(:time)
13
+ end
14
+
15
+ def deserialize(value)
16
+ value = value.iso8601 if value.is_a?(Date)
17
+ casted_value = caster.deserialize(value)
18
+
19
+ apply_precision(casted_value).in_time_zone if casted_value.acts_like?(:time)
20
+ end
21
+
22
+ private
23
+
24
+ def caster
25
+ ActiveRecord::Type::DateTime.new
26
+ end
27
+
28
+ # Use maximum precision by default to prevent the caster from truncating useful time information
29
+ # before precision is applied later
30
+ def precision
31
+ [options[:precision], MAX_PRECISION].compact.min
32
+ end
33
+
34
+ def apply_precision(value)
35
+ number_of_insignificant_digits = 9 - precision
36
+ round_power = 10**number_of_insignificant_digits
37
+ rounded_off_nsec = value.nsec % round_power
38
+
39
+ value.change(nsec: value.nsec - rounded_off_nsec)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -4,7 +4,8 @@ module ActiveFields
4
4
  module Casters
5
5
  class DecimalCaster < BaseCaster
6
6
  def serialize(value)
7
- cast(value)
7
+ # Decimals should be saved as strings to avoid potential precision loss when stored in JSON
8
+ cast(value)&.to_s
8
9
  end
9
10
 
10
11
  def deserialize(value)
@@ -14,7 +15,14 @@ module ActiveFields
14
15
  private
15
16
 
16
17
  def cast(value)
17
- BigDecimal(value, 0, exception: false)
18
+ casted = BigDecimal(value, 0, exception: false)
19
+ casted = casted.truncate(precision) if casted && precision
20
+
21
+ casted
22
+ end
23
+
24
+ def precision
25
+ options[:precision]
18
26
  end
19
27
  end
20
28
  end
@@ -2,6 +2,18 @@
2
2
 
3
3
  module ActiveFields
4
4
  module Casters
5
- class EnumArrayCaster < TextArrayCaster; end
5
+ class EnumArrayCaster < EnumCaster
6
+ def serialize(value)
7
+ return unless value.is_a?(Array)
8
+
9
+ value.map { super(_1) }
10
+ end
11
+
12
+ def deserialize(value)
13
+ return unless value.is_a?(Array)
14
+
15
+ value.map { super(_1) }
16
+ end
17
+ end
6
18
  end
7
19
  end
@@ -2,6 +2,20 @@
2
2
 
3
3
  module ActiveFields
4
4
  module Casters
5
- class EnumCaster < TextCaster; end
5
+ class EnumCaster < BaseCaster
6
+ def serialize(value)
7
+ cast(value)
8
+ end
9
+
10
+ def deserialize(value)
11
+ cast(value)
12
+ end
13
+
14
+ private
15
+
16
+ def cast(value)
17
+ value&.to_s.presence
18
+ end
19
+ end
6
20
  end
7
21
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveFields
4
4
  module Casters
5
- class IntegerCaster < DecimalCaster
5
+ class IntegerCaster < BaseCaster
6
6
  def serialize(value)
7
7
  cast(value)
8
8
  end
@@ -14,7 +14,7 @@ module ActiveFields
14
14
  private
15
15
 
16
16
  def cast(value)
17
- super&.to_i
17
+ BigDecimal(value, 0, exception: false)&.to_i
18
18
  end
19
19
  end
20
20
  end
@@ -7,7 +7,8 @@ module ActiveFields
7
7
 
8
8
  include Singleton
9
9
 
10
- attr_accessor :field_base_class_name, :value_class_name, :fields
10
+ attr_accessor :field_base_class_name, :value_class_name
11
+ attr_reader :fields
11
12
 
12
13
  def initialize
13
14
  @field_base_class_name = DEFAULT_FIELD_BASE_CLASS_NAME
@@ -16,6 +17,8 @@ module ActiveFields
16
17
  boolean: "ActiveFields::Field::Boolean",
17
18
  date: "ActiveFields::Field::Date",
18
19
  date_array: "ActiveFields::Field::DateArray",
20
+ datetime: "ActiveFields::Field::DateTime",
21
+ datetime_array: "ActiveFields::Field::DateTimeArray",
19
22
  decimal: "ActiveFields::Field::Decimal",
20
23
  decimal_array: "ActiveFields::Field::DecimalArray",
21
24
  enum: "ActiveFields::Field::Enum",
@@ -46,5 +49,13 @@ module ActiveFields
46
49
  def register_field(type_name, class_name)
47
50
  @fields[type_name.to_sym] = class_name
48
51
  end
52
+
53
+ def type_names
54
+ fields.keys
55
+ end
56
+
57
+ def type_class_names
58
+ fields.values
59
+ end
49
60
  end
50
61
  end
@@ -9,7 +9,7 @@ module ActiveFields
9
9
  end
10
10
 
11
11
  def types=(value)
12
- invalid_types = value - ActiveFields.config.fields.keys
12
+ invalid_types = value - ActiveFields.config.type_names
13
13
  if invalid_types.any?
14
14
  raise ArgumentError, "undefined ActiveFields types provided for #{customizable_model}: #{invalid_types}"
15
15
  end
@@ -8,7 +8,7 @@ module ActiveFields
8
8
  class_methods do
9
9
  attr_reader :active_fields_config
10
10
 
11
- def has_active_fields(types: ActiveFields.config.fields.keys)
11
+ def has_active_fields(types: ActiveFields.config.type_names)
12
12
  include CustomizableConcern
13
13
 
14
14
  @active_fields_config = CustomizableConfig.new(self)
@@ -4,24 +4,28 @@ module ActiveFields
4
4
  module Validators
5
5
  # Validates the active_value value
6
6
  class BaseValidator
7
- attr_reader :active_field, :errors
7
+ attr_reader :options, :errors
8
8
 
9
- def initialize(active_field)
10
- @active_field = active_field
9
+ def initialize(**options)
10
+ @options = options
11
11
  @errors = Set.new
12
12
  end
13
13
 
14
+ # Performs the validation and adds errors to the `errors` list.
15
+ # Returns `true` if no errors are found, `false` otherwise.
14
16
  def validate(value)
15
17
  perform_validation(value)
16
18
  valid?
17
19
  end
18
20
 
21
+ # Returns `true` if no errors are found, `false` otherwise.
19
22
  def valid?
20
23
  errors.empty?
21
24
  end
22
25
 
23
26
  private
24
27
 
28
+ # Performs the validation. If there are any errors, it should save them in `errors`.
25
29
  def perform_validation(value)
26
30
  raise NotImplementedError
27
31
  end
@@ -7,9 +7,9 @@ module ActiveFields
7
7
 
8
8
  def perform_validation(value)
9
9
  if value.nil?
10
- errors << :exclusion unless active_field.nullable?
10
+ errors << :exclusion unless options[:nullable]
11
11
  elsif value.is_a?(FalseClass)
12
- errors << :required if active_field.required?
12
+ errors << :required if options[:required]
13
13
  elsif value.is_a?(TrueClass)
14
14
  nil
15
15
  else