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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +633 -0
- data/app/models/active_fields/field/boolean.rb +10 -3
- data/app/models/active_fields/field/date.rb +10 -4
- data/app/models/active_fields/field/date_array.rb +12 -18
- data/app/models/active_fields/field/date_time.rb +77 -0
- data/app/models/active_fields/field/date_time_array.rb +61 -0
- data/app/models/active_fields/field/decimal.rb +35 -6
- data/app/models/active_fields/field/decimal_array.rb +28 -11
- data/app/models/active_fields/field/enum.rb +28 -9
- data/app/models/active_fields/field/enum_array.rb +29 -22
- data/app/models/active_fields/field/integer.rb +10 -4
- data/app/models/active_fields/field/integer_array.rb +12 -8
- data/app/models/active_fields/field/text.rb +10 -4
- data/app/models/active_fields/field/text_array.rb +14 -8
- data/app/models/concerns/active_fields/customizable_concern.rb +60 -51
- data/app/models/concerns/active_fields/field_array_concern.rb +25 -0
- data/app/models/concerns/active_fields/field_concern.rb +45 -24
- data/app/models/concerns/active_fields/value_concern.rb +32 -4
- data/db/migrate/20240229230000_create_active_fields_tables.rb +2 -2
- data/lib/active_fields/casters/base_caster.rb +8 -2
- data/lib/active_fields/casters/date_caster.rb +8 -6
- data/lib/active_fields/casters/date_time_array_caster.rb +19 -0
- data/lib/active_fields/casters/date_time_caster.rb +43 -0
- data/lib/active_fields/casters/decimal_caster.rb +10 -2
- data/lib/active_fields/casters/enum_array_caster.rb +13 -1
- data/lib/active_fields/casters/enum_caster.rb +15 -1
- data/lib/active_fields/casters/integer_caster.rb +2 -2
- data/lib/active_fields/config.rb +12 -1
- data/lib/active_fields/customizable_config.rb +1 -1
- data/lib/active_fields/has_active_fields.rb +1 -1
- data/lib/active_fields/validators/base_validator.rb +7 -3
- data/lib/active_fields/validators/boolean_validator.rb +2 -2
- data/lib/active_fields/validators/date_array_validator.rb +3 -3
- data/lib/active_fields/validators/date_time_array_validator.rb +26 -0
- data/lib/active_fields/validators/date_time_validator.rb +19 -0
- data/lib/active_fields/validators/date_validator.rb +3 -3
- data/lib/active_fields/validators/decimal_array_validator.rb +2 -2
- data/lib/active_fields/validators/decimal_validator.rb +2 -2
- data/lib/active_fields/validators/enum_array_validator.rb +3 -3
- data/lib/active_fields/validators/enum_validator.rb +3 -3
- data/lib/active_fields/validators/integer_array_validator.rb +2 -2
- data/lib/active_fields/validators/integer_validator.rb +2 -2
- data/lib/active_fields/validators/text_array_validator.rb +2 -2
- data/lib/active_fields/validators/text_validator.rb +2 -2
- data/lib/active_fields/version.rb +1 -1
- data/lib/active_fields.rb +4 -0
- data/lib/generators/active_fields/install/USAGE +5 -0
- data/lib/generators/active_fields/install/install_generator.rb +29 -0
- metadata +11 -16
@@ -1,85 +1,94 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveFields
|
4
|
-
#
|
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:
|
14
|
+
autosave: true,
|
18
15
|
dependent: :destroy
|
19
16
|
# rubocop:enable Rails/ReflectionClassName
|
20
17
|
|
21
|
-
|
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
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
50
|
+
# Handle `fields_for` params
|
51
|
+
attributes = attributes.values if attributes.is_a?(Hash)
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
next
|
61
|
+
active_field = active_fields_by_name[active_value_attributes[:name]]
|
62
|
+
next if active_field.nil?
|
63
63
|
|
64
|
-
active_value
|
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
|
-
|
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
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
82
|
-
|
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
|
-
#
|
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
|
-
|
30
|
-
|
31
|
-
|
28
|
+
class_methods do
|
29
|
+
def acts_as_active_field(array: false, validator:, caster:)
|
30
|
+
include FieldArrayConcern if array
|
32
31
|
|
33
|
-
|
34
|
-
value_validator_class.new(self)
|
35
|
-
end
|
32
|
+
define_method(:array?) { array }
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
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.
|
69
|
+
customizable_type.safe_constantize
|
47
70
|
end
|
48
71
|
|
49
72
|
def default_value=(v)
|
50
|
-
|
73
|
+
default_value_meta["const"] = value_caster.serialize(v)
|
51
74
|
end
|
52
75
|
|
53
76
|
def default_value
|
54
|
-
value_caster.deserialize(
|
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
|
76
|
-
return true if
|
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
|
-
#
|
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 :
|
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
|
-
|
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
|
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 :
|
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 :
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
17
|
-
|
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
|
-
|
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 <
|
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 <
|
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 <
|
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
|
-
|
17
|
+
BigDecimal(value, 0, exception: false)&.to_i
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
data/lib/active_fields/config.rb
CHANGED
@@ -7,7 +7,8 @@ module ActiveFields
|
|
7
7
|
|
8
8
|
include Singleton
|
9
9
|
|
10
|
-
attr_accessor :field_base_class_name, :value_class_name
|
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.
|
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.
|
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 :
|
7
|
+
attr_reader :options, :errors
|
8
8
|
|
9
|
-
def initialize(
|
10
|
-
@
|
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
|
10
|
+
errors << :exclusion unless options[:nullable]
|
11
11
|
elsif value.is_a?(FalseClass)
|
12
|
-
errors << :required if
|
12
|
+
errors << :required if options[:required]
|
13
13
|
elsif value.is_a?(TrueClass)
|
14
14
|
nil
|
15
15
|
else
|