active_fields 0.1.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/.rspec +0 -1
- data/.rubocop.yml +3 -15
- data/CHANGELOG.md +19 -0
- data/README.md +633 -0
- data/Rakefile +7 -0
- data/app/models/active_fields/application_record.rb +7 -0
- data/app/models/active_fields/field/base.rb +17 -0
- data/{lib/active_fields/models → app/models/active_fields}/field/boolean.rb +14 -9
- data/{lib/active_fields/models → app/models/active_fields}/field/date.rb +16 -12
- data/app/models/active_fields/field/date_array.rb +36 -0
- 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 +75 -0
- data/app/models/active_fields/field/decimal_array.rb +59 -0
- data/{lib/active_fields/models → app/models/active_fields}/field/enum.rb +34 -17
- data/app/models/active_fields/field/enum_array.rb +64 -0
- data/{lib/active_fields/models → app/models/active_fields}/field/integer.rb +16 -12
- data/app/models/active_fields/field/integer_array.rb +36 -0
- data/{lib/active_fields/models → app/models/active_fields}/field/text.rb +16 -12
- data/app/models/active_fields/field/text_array.rb +39 -0
- data/app/models/active_fields/value.rb +15 -0
- data/app/models/concerns/active_fields/customizable_concern.rb +94 -0
- data/app/models/concerns/active_fields/field_array_concern.rb +25 -0
- data/app/models/concerns/active_fields/field_concern.rb +111 -0
- data/app/models/concerns/active_fields/value_concern.rb +83 -0
- data/config/routes.rb +4 -0
- data/{lib/generators/active_fields/install/templates/create_active_fields_tables.rb.tt → db/migrate/20240229230000_create_active_fields_tables.rb} +8 -6
- data/lib/active_fields/casters/base_caster.rb +8 -2
- data/lib/active_fields/casters/boolean_caster.rb +0 -2
- data/lib/active_fields/casters/date_array_caster.rb +0 -2
- data/lib/active_fields/casters/date_caster.rb +8 -8
- 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_array_caster.rb +0 -2
- data/lib/active_fields/casters/decimal_caster.rb +10 -4
- data/lib/active_fields/casters/enum_array_caster.rb +13 -3
- data/lib/active_fields/casters/enum_caster.rb +15 -3
- data/lib/active_fields/casters/integer_array_caster.rb +0 -2
- data/lib/active_fields/casters/integer_caster.rb +2 -4
- data/lib/active_fields/casters/text_array_caster.rb +0 -2
- data/lib/active_fields/casters/text_caster.rb +0 -2
- data/lib/active_fields/config.rb +61 -0
- data/lib/active_fields/customizable_config.rb +24 -0
- data/lib/active_fields/engine.rb +13 -0
- data/lib/active_fields/has_active_fields.rb +19 -0
- data/lib/active_fields/validators/base_validator.rb +7 -3
- data/lib/active_fields/validators/boolean_validator.rb +2 -4
- data/lib/active_fields/validators/date_array_validator.rb +3 -5
- 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 -5
- data/lib/active_fields/validators/decimal_array_validator.rb +2 -4
- data/lib/active_fields/validators/decimal_validator.rb +2 -4
- data/lib/active_fields/validators/enum_array_validator.rb +3 -5
- data/lib/active_fields/validators/enum_validator.rb +3 -5
- data/lib/active_fields/validators/integer_array_validator.rb +2 -4
- data/lib/active_fields/validators/integer_validator.rb +2 -4
- data/lib/active_fields/validators/text_array_validator.rb +2 -4
- data/lib/active_fields/validators/text_validator.rb +2 -4
- data/lib/active_fields/version.rb +1 -1
- data/lib/active_fields.rb +47 -7
- data/lib/generators/active_fields/install/USAGE +1 -1
- data/lib/generators/active_fields/install/install_generator.rb +14 -18
- data/lib/tasks/active_fields_tasks.rake +6 -0
- metadata +49 -36
- data/lib/active_fields/models/concerns/concern.rb +0 -16
- data/lib/active_fields/models/concerns/customizable.rb +0 -83
- data/lib/active_fields/models/concerns/field_concern.rb +0 -79
- data/lib/active_fields/models/concerns/value_concern.rb +0 -50
- data/lib/active_fields/models/field/date_array.rb +0 -44
- data/lib/active_fields/models/field/decimal.rb +0 -48
- data/lib/active_fields/models/field/decimal_array.rb +0 -44
- data/lib/active_fields/models/field/enum_array.rb +0 -59
- data/lib/active_fields/models/field/integer_array.rb +0 -34
- data/lib/active_fields/models/field/text_array.rb +0 -35
- data/lib/active_fields/models/field.rb +0 -27
- data/lib/active_fields/models/value.rb +0 -30
@@ -1,15 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../field"
|
4
|
-
|
5
3
|
module ActiveFields
|
6
|
-
|
7
|
-
class Text < ActiveFields
|
8
|
-
|
4
|
+
module Field
|
5
|
+
class Text < ActiveFields.config.field_base_class
|
6
|
+
acts_as_active_field(
|
7
|
+
validator: {
|
8
|
+
class_name: "ActiveFields::Validators::TextValidator",
|
9
|
+
options: -> { { required: required?, min_length: min_length, max_length: max_length } },
|
10
|
+
},
|
11
|
+
caster: {
|
12
|
+
class_name: "ActiveFields::Casters::TextCaster",
|
13
|
+
},
|
14
|
+
)
|
9
15
|
|
10
|
-
|
11
|
-
# attribute :min_length, :integer
|
12
|
-
# attribute :max_length, :integer
|
16
|
+
store_accessor :options, :required, :min_length, :max_length
|
13
17
|
|
14
18
|
validates :required, exclusion: [nil]
|
15
19
|
validates :min_length, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true
|
@@ -17,7 +21,7 @@ module ActiveFields
|
|
17
21
|
|
18
22
|
%i[required].each do |column|
|
19
23
|
define_method(column) do
|
20
|
-
|
24
|
+
Casters::BooleanCaster.new.deserialize(super())
|
21
25
|
end
|
22
26
|
|
23
27
|
define_method(:"#{column}?") do
|
@@ -25,17 +29,17 @@ module ActiveFields
|
|
25
29
|
end
|
26
30
|
|
27
31
|
define_method(:"#{column}=") do |other|
|
28
|
-
super(
|
32
|
+
super(Casters::BooleanCaster.new.serialize(other))
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
32
36
|
%i[min_length max_length].each do |column|
|
33
37
|
define_method(column) do
|
34
|
-
|
38
|
+
Casters::IntegerCaster.new.deserialize(super())
|
35
39
|
end
|
36
40
|
|
37
41
|
define_method(:"#{column}=") do |other|
|
38
|
-
super(
|
42
|
+
super(Casters::IntegerCaster.new.serialize(other))
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
module Field
|
5
|
+
class TextArray < ActiveFields.config.field_base_class
|
6
|
+
acts_as_active_field(
|
7
|
+
array: true,
|
8
|
+
validator: {
|
9
|
+
class_name: "ActiveFields::Validators::TextArrayValidator",
|
10
|
+
options: -> do
|
11
|
+
{ min_length: min_length, max_length: max_length, min_size: min_size, max_size: max_size }
|
12
|
+
end,
|
13
|
+
},
|
14
|
+
caster: {
|
15
|
+
class_name: "ActiveFields::Casters::TextArrayCaster",
|
16
|
+
},
|
17
|
+
)
|
18
|
+
|
19
|
+
store_accessor :options, :min_length, :max_length
|
20
|
+
|
21
|
+
validates :min_length, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true
|
22
|
+
validates :max_length, comparison: { greater_than_or_equal_to: ->(r) { r.min_length || 0 } }, allow_nil: true
|
23
|
+
|
24
|
+
%i[min_length max_length].each do |column|
|
25
|
+
define_method(column) do
|
26
|
+
Casters::IntegerCaster.new.deserialize(super())
|
27
|
+
end
|
28
|
+
|
29
|
+
define_method(:"#{column}=") do |other|
|
30
|
+
super(Casters::IntegerCaster.new.serialize(other))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_defaults; end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
# If value base class has been changed, we should prevent this model from being loaded.
|
5
|
+
# Since we cannot remove it entirely, we will not add any functionality to it.
|
6
|
+
if ActiveFields.config.value_class_changed?
|
7
|
+
class Value; end
|
8
|
+
else
|
9
|
+
class Value < ApplicationRecord
|
10
|
+
self.table_name = "active_fields_values"
|
11
|
+
|
12
|
+
include ValueConcern
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
# Model mix-in that adds the active fields functionality to the customizable model
|
5
|
+
module CustomizableConcern
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# rubocop:disable Rails/ReflectionClassName
|
10
|
+
has_many :active_values,
|
11
|
+
class_name: ActiveFields.config.value_class_name,
|
12
|
+
as: :customizable,
|
13
|
+
inverse_of: :customizable,
|
14
|
+
autosave: true,
|
15
|
+
dependent: :destroy
|
16
|
+
# rubocop:enable Rails/ReflectionClassName
|
17
|
+
|
18
|
+
accepts_nested_attributes_for :active_values, allow_destroy: true
|
19
|
+
end
|
20
|
+
|
21
|
+
def active_fields
|
22
|
+
ActiveFields.config.field_base_class.for(model_name.name)
|
23
|
+
end
|
24
|
+
|
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
|
49
|
+
|
50
|
+
# Handle `fields_for` params
|
51
|
+
attributes = attributes.values if attributes.is_a?(Hash)
|
52
|
+
|
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
|
+
|
61
|
+
active_field = active_fields_by_name[active_value_attributes[:name]]
|
62
|
+
next if active_field.nil?
|
63
|
+
|
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
|
76
|
+
end
|
77
|
+
|
78
|
+
self.active_values_attributes = nested_attributes
|
79
|
+
end
|
80
|
+
|
81
|
+
alias_method :active_fields=, :active_fields_attributes=
|
82
|
+
|
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)
|
86
|
+
|
87
|
+
active_fields.each do |active_field|
|
88
|
+
next if existing_field_ids.include?(active_field.id)
|
89
|
+
|
90
|
+
active_values.new(active_field: active_field, value: active_field.default_value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
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
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
# Model mix-in with a base logic for the active fields model
|
5
|
+
module FieldConcern
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# rubocop:disable Rails/ReflectionClassName
|
10
|
+
has_many :active_values,
|
11
|
+
class_name: ActiveFields.config.value_class_name,
|
12
|
+
foreign_key: :active_field_id,
|
13
|
+
inverse_of: :active_field,
|
14
|
+
dependent: :destroy
|
15
|
+
# rubocop:enable Rails/ReflectionClassName
|
16
|
+
|
17
|
+
scope :for, ->(customizable_type) { where(customizable_type: customizable_type) }
|
18
|
+
|
19
|
+
validates :type, presence: true
|
20
|
+
validates :name, presence: true, uniqueness: { scope: :customizable_type }
|
21
|
+
validates :name, format: { with: /\A[a-z0-9_]+\z/ }, allow_blank: true
|
22
|
+
validate :validate_default_value
|
23
|
+
validate :validate_customizable_model_allows_type
|
24
|
+
|
25
|
+
after_initialize :set_defaults
|
26
|
+
end
|
27
|
+
|
28
|
+
class_methods do
|
29
|
+
def acts_as_active_field(array: false, validator:, caster:)
|
30
|
+
include FieldArrayConcern if array
|
31
|
+
|
32
|
+
define_method(:array?) { array }
|
33
|
+
|
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
|
53
|
+
|
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
|
66
|
+
end
|
67
|
+
|
68
|
+
def customizable_model
|
69
|
+
customizable_type.safe_constantize
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_value=(v)
|
73
|
+
default_value_meta["const"] = value_caster.serialize(v)
|
74
|
+
end
|
75
|
+
|
76
|
+
def default_value
|
77
|
+
value_caster.deserialize(default_value_meta["const"])
|
78
|
+
end
|
79
|
+
|
80
|
+
def type_name
|
81
|
+
ActiveFields.config.fields.key(type)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def validate_default_value
|
87
|
+
validator = value_validator
|
88
|
+
return if validator.validate(default_value)
|
89
|
+
|
90
|
+
validator.errors.each do |error|
|
91
|
+
if error.is_a?(Array) && error.size == 2 && error.first.is_a?(Symbol) && error.last.is_a?(Hash)
|
92
|
+
errors.add(:default_value, error.first, **error.last)
|
93
|
+
elsif error.is_a?(Symbol)
|
94
|
+
errors.add(:default_value, *error)
|
95
|
+
else
|
96
|
+
raise ArgumentError
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_customizable_model_allows_type
|
102
|
+
allowed_types = customizable_model&.active_fields_config&.types || []
|
103
|
+
return true if allowed_types.include?(type_name)
|
104
|
+
|
105
|
+
errors.add(:customizable_type, :inclusion)
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_defaults; end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
# Model mix-in with a base logic for the active fields value model
|
5
|
+
module ValueConcern
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
belongs_to :customizable, polymorphic: true, optional: false, inverse_of: :active_values
|
10
|
+
# rubocop:disable Rails/ReflectionClassName
|
11
|
+
belongs_to :active_field,
|
12
|
+
class_name: ActiveFields.config.field_base_class_name,
|
13
|
+
optional: false,
|
14
|
+
inverse_of: :active_values
|
15
|
+
# rubocop:enable Rails/ReflectionClassName
|
16
|
+
|
17
|
+
validates :active_field, uniqueness: { scope: :customizable }
|
18
|
+
validate :validate_value
|
19
|
+
validate :validate_customizable_allowed
|
20
|
+
|
21
|
+
before_validation :assign_value_from_temp, if: -> { temp_value && active_field }
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate :name, to: :active_field, allow_nil: true
|
25
|
+
|
26
|
+
attr_reader :temp_value
|
27
|
+
|
28
|
+
def value=(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
|
35
|
+
end
|
36
|
+
|
37
|
+
def value
|
38
|
+
return unless active_field
|
39
|
+
|
40
|
+
assign_value_from_temp if temp_value
|
41
|
+
|
42
|
+
active_field.value_caster.deserialize(value_meta["const"])
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def validate_value
|
48
|
+
return if (validator = active_field&.value_validator).nil?
|
49
|
+
return if validator.validate(value)
|
50
|
+
|
51
|
+
validator.errors.each do |error|
|
52
|
+
if error.is_a?(Array) && error.size == 2 && error.first.is_a?(Symbol) && error.last.is_a?(Hash)
|
53
|
+
errors.add(:value, error.first, **error.last)
|
54
|
+
elsif error.is_a?(Symbol)
|
55
|
+
errors.add(:value, *error)
|
56
|
+
else
|
57
|
+
raise ArgumentError
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_customizable_allowed
|
63
|
+
return true if active_field.nil?
|
64
|
+
return true if customizable_type == active_field.customizable_type
|
65
|
+
|
66
|
+
errors.add(:customizable, :invalid)
|
67
|
+
false
|
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
|
82
|
+
end
|
83
|
+
end
|
data/config/routes.rb
ADDED
@@ -1,15 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateActiveFieldsTables < ActiveRecord::Migration[6.0]
|
2
4
|
def change
|
3
5
|
create_table :active_fields do |t|
|
4
6
|
t.string :name, null: false
|
5
7
|
t.string :type, null: false
|
6
8
|
t.string :customizable_type, null: false
|
7
|
-
t.jsonb :
|
9
|
+
t.jsonb :default_value_meta, null: false, default: {}
|
8
10
|
t.jsonb :options, null: false, default: {}
|
9
11
|
|
10
12
|
t.timestamps
|
11
13
|
|
12
|
-
t.index [
|
14
|
+
t.index %i[name customizable_type], unique: true
|
13
15
|
t.index :customizable_type
|
14
16
|
end
|
15
17
|
|
@@ -17,12 +19,12 @@ class CreateActiveFieldsTables < ActiveRecord::Migration<%= migration_version %>
|
|
17
19
|
t.references :customizable, polymorphic: true, null: false, index: false
|
18
20
|
t.references :active_field,
|
19
21
|
null: false,
|
20
|
-
foreign_key: { to_table: :active_fields, name:
|
21
|
-
t.jsonb :
|
22
|
+
foreign_key: { to_table: :active_fields, name: "active_fields_values_active_field_id_fk" }
|
23
|
+
t.jsonb :value_meta, null: false, default: {}
|
22
24
|
|
23
25
|
t.timestamps
|
24
26
|
|
25
|
-
t.index [
|
27
|
+
t.index %i[customizable_type customizable_id active_field_id],
|
26
28
|
unique: true,
|
27
29
|
name: "index_active_fields_values_on_customizable_and_field"
|
28
30
|
end
|
@@ -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
|
@@ -1,24 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "base_caster"
|
4
|
-
|
5
3
|
module ActiveFields
|
6
4
|
module Casters
|
7
5
|
class DateCaster < BaseCaster
|
8
6
|
def serialize(value)
|
9
|
-
|
7
|
+
casted_value = caster.serialize(value)
|
8
|
+
|
9
|
+
casted_value.iso8601 if casted_value.is_a?(Date)
|
10
10
|
end
|
11
11
|
|
12
12
|
def deserialize(value)
|
13
|
-
|
13
|
+
casted_value = caster.deserialize(value)
|
14
|
+
|
15
|
+
casted_value if casted_value.is_a?(Date)
|
14
16
|
end
|
15
17
|
|
16
18
|
private
|
17
19
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
casted_value if casted_value.acts_like?(:date)
|
20
|
+
def caster
|
21
|
+
ActiveRecord::Type::Date.new
|
22
22
|
end
|
23
23
|
end
|
24
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
|
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "base_caster"
|
4
|
-
|
5
3
|
module ActiveFields
|
6
4
|
module Casters
|
7
5
|
class DecimalCaster < BaseCaster
|
8
6
|
def serialize(value)
|
9
|
-
|
7
|
+
# Decimals should be saved as strings to avoid potential precision loss when stored in JSON
|
8
|
+
cast(value)&.to_s
|
10
9
|
end
|
11
10
|
|
12
11
|
def deserialize(value)
|
@@ -16,7 +15,14 @@ module ActiveFields
|
|
16
15
|
private
|
17
16
|
|
18
17
|
def cast(value)
|
19
|
-
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]
|
20
26
|
end
|
21
27
|
end
|
22
28
|
end
|
@@ -1,9 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "text_array_caster"
|
4
|
-
|
5
3
|
module ActiveFields
|
6
4
|
module Casters
|
7
|
-
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
|
8
18
|
end
|
9
19
|
end
|