active_fields 0.1.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/.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
|