active_fields 0.2.0 → 1.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 (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