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.
- 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
|