diff --git a/.rubocop.yml b/.rubocop.yml index edc87a5..5a31fbf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,6 +31,9 @@ Style/WordArray: Style/MethodCallWithArgsParentheses: Enabled: false +Style/SafeNavigationChainLength: + Enabled: false + Metrics/BlockLength: Exclude: - "spec/**/*" diff --git a/CHANGELOG.md b/CHANGELOG.md index 630c57c..ccdcdac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ - Drop support for Rails < 7 (EOL) - Drop support for Ruby < 3.1 (EOL) +**Breaking changes**: +- Maximum datetime precision reduced to 6 for all _Ruby_/_Rails_ versions. + + While _Ruby_ allows up to 9 fractional seconds, most databases, including _PostgreSQL_, support only 6. + To ensure compatibility and prevent potential issues, + we are standardizing the precision to the minimum supported across our technology stack. + +- Maximum datetime precision constant relocated. + + The maximum precision value has been moved + from `ActiveFields::Casters::DateTimeCaster::MAX_PRECISION` to `ActiveFields::MAX_DATETIME_PRECISION`. + +- Maximum decimal precision set to to 16383 (2**14 - 1). + + While _Ruby_'s `BigDecimal` class allows extremely high precision, + PostgreSQL supports a maximum of 16383 digits after the decimal point. + To ensure compatibility, we are capping the precision at this value. + The maximum precision value is now accessible via `ActiveFields::MAX_DECIMAL_PRECISION`. + ## [1.1.0] - 2024-09-10 - Added scaffold generator - Disabled models reloading to prevent STI issues diff --git a/app/models/active_fields/field/date_time.rb b/app/models/active_fields/field/date_time.rb index fcdc78e..a1be9fa 100644 --- a/app/models/active_fields/field/date_time.rb +++ b/app/models/active_fields/field/date_time.rb @@ -19,7 +19,7 @@ class DateTime < ActiveFields.config.field_base_class validates :required, exclusion: [nil] validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min validates :precision, - comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: Casters::DateTimeCaster::MAX_PRECISION }, + comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION }, allow_nil: true # If precision is set after attributes that depend on it, deserialization will work correctly, diff --git a/app/models/active_fields/field/date_time_array.rb b/app/models/active_fields/field/date_time_array.rb index 2bd6b5e..b7dbba1 100644 --- a/app/models/active_fields/field/date_time_array.rb +++ b/app/models/active_fields/field/date_time_array.rb @@ -19,7 +19,7 @@ class DateTimeArray < ActiveFields.config.field_base_class validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min validates :precision, - comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: Casters::DateTimeCaster::MAX_PRECISION }, + comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION }, allow_nil: true # If precision is set after attributes that depend on it, deserialization will work correctly, diff --git a/app/models/active_fields/field/decimal.rb b/app/models/active_fields/field/decimal.rb index 6b8b94a..42c1bf5 100644 --- a/app/models/active_fields/field/decimal.rb +++ b/app/models/active_fields/field/decimal.rb @@ -18,7 +18,9 @@ class Decimal < ActiveFields.config.field_base_class validates :required, exclusion: [nil] validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min - validates :precision, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true + validates :precision, + comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION }, + allow_nil: true # If precision is set after attributes that depend on it, deserialization will work correctly, # but an incorrect internal value may be saved in the DB. diff --git a/app/models/active_fields/field/decimal_array.rb b/app/models/active_fields/field/decimal_array.rb index 568be87..76d1467 100644 --- a/app/models/active_fields/field/decimal_array.rb +++ b/app/models/active_fields/field/decimal_array.rb @@ -18,7 +18,9 @@ class DecimalArray < ActiveFields.config.field_base_class store_accessor :options, :min, :max, :precision validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min - validates :precision, comparison: { greater_than_or_equal_to: 0 }, allow_nil: true + validates :precision, + comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION }, + allow_nil: true # If precision is set after attributes that depend on it, deserialization will work correctly, # but an incorrect internal value may be saved in the DB. diff --git a/lib/active_fields.rb b/lib/active_fields.rb index f9a1b85..4c8bf37 100644 --- a/lib/active_fields.rb +++ b/lib/active_fields.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "active_fields/version" +require_relative "active_fields/constants" require_relative "active_fields/engine" module ActiveFields diff --git a/lib/active_fields/casters/date_time_caster.rb b/lib/active_fields/casters/date_time_caster.rb index 9257925..95adae9 100644 --- a/lib/active_fields/casters/date_time_caster.rb +++ b/lib/active_fields/casters/date_time_caster.rb @@ -3,8 +3,6 @@ module ActiveFields module Casters class DateTimeCaster < BaseCaster - MAX_PRECISION = RUBY_VERSION >= "3.2" ? 9 : 6 # AR max precision is 6 for old Rubies - def serialize(value) value = value.iso8601 if value.is_a?(Date) casted_value = caster.serialize(value) @@ -28,7 +26,7 @@ def caster # Use maximum precision by default to prevent the caster from truncating useful time information # before precision is applied later def precision - [options[:precision], MAX_PRECISION].compact.min + [options[:precision], ActiveFields::MAX_DATETIME_PRECISION].compact.min end def apply_precision(value) diff --git a/lib/active_fields/casters/decimal_caster.rb b/lib/active_fields/casters/decimal_caster.rb index d14663e..d12326a 100644 --- a/lib/active_fields/casters/decimal_caster.rb +++ b/lib/active_fields/casters/decimal_caster.rb @@ -15,14 +15,11 @@ def deserialize(value) private def cast(value) - casted = BigDecimal(value, 0, exception: false) - casted = casted.truncate(precision) if casted && precision - - casted + BigDecimal(value, 0, exception: false)&.truncate(precision) end def precision - options[:precision] + [options[:precision], ActiveFields::MAX_DECIMAL_PRECISION].compact.min end end end diff --git a/lib/active_fields/constants.rb b/lib/active_fields/constants.rb new file mode 100644 index 0000000..6c4bec4 --- /dev/null +++ b/lib/active_fields/constants.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module ActiveFields + # Ruby supports up to 9 fractional seconds, but PostgreSQL, like most databases, supports only 6. + # Since we use PostgreSQL, we standardize on 6. + MAX_DATETIME_PRECISION = 6 + + # Ruby's BigDecimal class allows extremely high precision, + # but PostgreSQL supports a maximum of 16383 digits after the decimal point. + # Since we use PostgreSQL, we limit decimal precision to 16383. + MAX_DECIMAL_PRECISION = 2**14 - 1 +end diff --git a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb index c6ad467..7bc9229 100644 --- a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +++ b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb @@ -49,7 +49,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, max: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DATETIME_PRECISION, disabled: active_field.persisted? %>
diff --git a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb index d2675a8..a7f2e5b 100644 --- a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +++ b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb @@ -54,7 +54,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, max: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DATETIME_PRECISION, disabled: active_field.persisted? %>
diff --git a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb index e394236..92f098c 100644 --- a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +++ b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb @@ -49,7 +49,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DECIMAL_PRECISION, disabled: active_field.persisted? %>
diff --git a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb index 6e50877..dce9214 100644 --- a/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +++ b/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb @@ -54,7 +54,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DECIMAL_PRECISION, disabled: active_field.persisted? %>
diff --git a/spec/dummy/app/views/active_fields/forms/_datetime.html.erb b/spec/dummy/app/views/active_fields/forms/_datetime.html.erb index fd9ecdb..4f9ae82 100644 --- a/spec/dummy/app/views/active_fields/forms/_datetime.html.erb +++ b/spec/dummy/app/views/active_fields/forms/_datetime.html.erb @@ -39,7 +39,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, max: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DATETIME_PRECISION, disabled: active_field.persisted? %>
diff --git a/spec/dummy/app/views/active_fields/forms/_datetime_array.html.erb b/spec/dummy/app/views/active_fields/forms/_datetime_array.html.erb index b432d34..c6d7ddc 100644 --- a/spec/dummy/app/views/active_fields/forms/_datetime_array.html.erb +++ b/spec/dummy/app/views/active_fields/forms/_datetime_array.html.erb @@ -44,7 +44,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, max: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DATETIME_PRECISION, disabled: active_field.persisted? %>
diff --git a/spec/dummy/app/views/active_fields/forms/_decimal.html.erb b/spec/dummy/app/views/active_fields/forms/_decimal.html.erb index 5d1eb2b..7fa561a 100644 --- a/spec/dummy/app/views/active_fields/forms/_decimal.html.erb +++ b/spec/dummy/app/views/active_fields/forms/_decimal.html.erb @@ -39,7 +39,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DECIMAL_PRECISION, disabled: active_field.persisted? %>
diff --git a/spec/dummy/app/views/active_fields/forms/_decimal_array.html.erb b/spec/dummy/app/views/active_fields/forms/_decimal_array.html.erb index 0c697c0..6697f10 100644 --- a/spec/dummy/app/views/active_fields/forms/_decimal_array.html.erb +++ b/spec/dummy/app/views/active_fields/forms/_decimal_array.html.erb @@ -44,7 +44,7 @@
<%= f.label :precision %> - <%= f.number_field :precision, min: 0, disabled: active_field.persisted? %> + <%= f.number_field :precision, min: 0, max: ActiveFields::MAX_DECIMAL_PRECISION, disabled: active_field.persisted? %>
diff --git a/spec/factories/active_fields.rb b/spec/factories/active_fields.rb index 4d458db..75c6701 100644 --- a/spec/factories/active_fields.rb +++ b/spec/factories/active_fields.rb @@ -77,7 +77,7 @@ end trait :with_precision do - precision { rand(0..ActiveFields::Casters::DateTimeCaster::MAX_PRECISION) } + precision { rand(0..ActiveFields::MAX_DATETIME_PRECISION) } end end @@ -101,7 +101,7 @@ end trait :with_precision do - precision { rand(0..ActiveFields::Casters::DateTimeCaster::MAX_PRECISION) } + precision { rand(0..ActiveFields::MAX_DATETIME_PRECISION) } end end @@ -121,7 +121,7 @@ end trait :with_precision do - precision { rand(0..10) } + precision { rand(0..ActiveFields::MAX_DECIMAL_PRECISION) } end end @@ -145,7 +145,7 @@ end trait :with_precision do - precision { rand(0..10) } + precision { rand(0..ActiveFields::MAX_DECIMAL_PRECISION) } end end diff --git a/spec/lib/active_fields/casters/date_time_array_caster_spec.rb b/spec/lib/active_fields/casters/date_time_array_caster_spec.rb index 152b5d5..a5665b1 100644 --- a/spec/lib/active_fields/casters/date_time_array_caster_spec.rb +++ b/spec/lib/active_fields/casters/date_time_array_caster_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe ActiveFields::Casters::DateTimeArrayCaster do - max_precision = ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + max_precision = ActiveFields::MAX_DATETIME_PRECISION let(:object) { described_class.new(**args) } let(:args) { {} } @@ -59,7 +59,7 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision)] } + let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision + 1)] } it { is_expected.to eq(value.map { Time.zone.parse(_1).utc.iso8601(max_precision) }) } end @@ -91,7 +91,7 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision)] } + let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision + 1)] } it { is_expected.to eq(value.map { Time.zone.parse(_1).utc.iso8601(max_precision) }) } end @@ -119,7 +119,7 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision)] } + let(:value) { [random_datetime.iso8601, random_datetime.iso8601(max_precision + 1)] } it { is_expected.to eq(value.map { Time.zone.parse(_1).utc.iso8601(args[:precision]) }) } end @@ -163,13 +163,17 @@ context "when array of dates" do let(:value) { [random_date, random_date] } - it { is_expected.to eq(value.map { _1.to_time(:utc).in_time_zone }) } + it { is_expected.to eq(value.map { apply_datetime_precision(_1.to_time(:utc).in_time_zone, max_precision) }) } end context "when array of date strings" do let(:value) { [random_date.iso8601, random_date.iso8601] } - it { is_expected.to eq(value.map { Date.parse(_1).to_time(:utc).in_time_zone }) } + it do + expect(call_method).to eq( + value.map { apply_datetime_precision(Date.parse(_1).to_time(:utc).in_time_zone, max_precision) }, + ) + end end context "when array of datetimes" do @@ -179,7 +183,7 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision)] } + let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision + 1)] } it "casts elements to time objects with max precision" do expect(call_method).to eq( @@ -199,13 +203,17 @@ context "when array of dates" do let(:value) { [random_date, random_date] } - it { is_expected.to eq(value.map { _1.to_time(:utc).in_time_zone }) } + it { is_expected.to eq(value.map { apply_datetime_precision(_1.to_time(:utc).in_time_zone, max_precision) }) } end context "when array of date strings" do let(:value) { [random_date.iso8601, random_date.iso8601] } - it { is_expected.to eq(value.map { Date.parse(_1).to_time(:utc).in_time_zone }) } + it do + expect(call_method).to eq( + value.map { apply_datetime_precision(Date.parse(_1).to_time(:utc).in_time_zone, max_precision) }, + ) + end end context "when array of datetimes" do @@ -215,9 +223,9 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision)] } + let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision + 1)] } - it "casts elements to time objects with max precision" do + it do expect(call_method).to eq( value.map { apply_datetime_precision(Time.zone.parse(_1).in_time_zone, max_precision) }, ) @@ -231,13 +239,21 @@ context "when array of dates" do let(:value) { [random_date, random_date] } - it { is_expected.to eq(value.map { _1.to_time(:utc).in_time_zone }) } + it do + expect(call_method).to eq( + value.map { apply_datetime_precision(_1.to_time(:utc).in_time_zone, args[:precision]) }, + ) + end end context "when array of date strings" do let(:value) { [random_date.iso8601, random_date.iso8601] } - it { is_expected.to eq(value.map { Date.parse(_1).to_time(:utc).in_time_zone }) } + it do + expect(call_method).to eq( + value.map { apply_datetime_precision(Date.parse(_1).to_time(:utc).in_time_zone, args[:precision]) }, + ) + end end context "when array of datetimes" do @@ -247,7 +263,7 @@ end context "when array of datetime strings" do - let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision)] } + let(:value) { [random_datetime.utc.iso8601, random_datetime.utc.iso8601(max_precision + 1)] } it "casts elements to time objects with provided precision" do expect(call_method).to eq( diff --git a/spec/lib/active_fields/casters/date_time_caster_spec.rb b/spec/lib/active_fields/casters/date_time_caster_spec.rb index ee68f1d..4353a61 100644 --- a/spec/lib/active_fields/casters/date_time_caster_spec.rb +++ b/spec/lib/active_fields/casters/date_time_caster_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe ActiveFields::Casters::DateTimeCaster do - max_precision = ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + max_precision = ActiveFields::MAX_DATETIME_PRECISION let(:object) { described_class.new(**args) } let(:args) { {} } @@ -59,7 +59,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.iso8601(max_precision) } + let(:value) { random_datetime.iso8601(max_precision + 1) } it { is_expected.to eq(Time.zone.parse(value).utc.iso8601(max_precision)) } end @@ -97,7 +97,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.iso8601(max_precision) } + let(:value) { random_datetime.iso8601(max_precision + 1) } it { is_expected.to eq(Time.zone.parse(value).utc.iso8601(max_precision)) } end @@ -131,7 +131,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.iso8601(max_precision) } + let(:value) { random_datetime.iso8601(max_precision + 1) } it { is_expected.to eq(Time.zone.parse(value).utc.iso8601(args[:precision])) } end @@ -169,13 +169,13 @@ context "when date" do let(:value) { random_date } - it { is_expected.to eq(value.to_time(:utc).in_time_zone) } + it { is_expected.to eq(apply_datetime_precision(value.to_time(:utc).in_time_zone, max_precision)) } end context "when date string" do let(:value) { random_date.iso8601 } - it { is_expected.to eq(Date.parse(value).to_time(:utc).in_time_zone) } + it { is_expected.to eq(apply_datetime_precision(Date.parse(value).to_time(:utc).in_time_zone, max_precision)) } end context "when datetime" do @@ -191,7 +191,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.utc.iso8601(max_precision) } + let(:value) { random_datetime.utc.iso8601(max_precision + 1) } it { is_expected.to eq(apply_datetime_precision(Time.zone.parse(value).in_time_zone, max_precision)) } end @@ -207,13 +207,13 @@ context "when date" do let(:value) { random_date } - it { is_expected.to eq(value.to_time(:utc).in_time_zone) } + it { is_expected.to eq(apply_datetime_precision(value.to_time(:utc).in_time_zone, max_precision)) } end context "when date string" do let(:value) { random_date.iso8601 } - it { is_expected.to eq(Date.parse(value).to_time(:utc).in_time_zone) } + it { is_expected.to eq(apply_datetime_precision(Date.parse(value).to_time(:utc).in_time_zone, max_precision)) } end context "when datetime" do @@ -229,7 +229,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.utc.iso8601(max_precision) } + let(:value) { random_datetime.utc.iso8601(max_precision + 1) } it { is_expected.to eq(apply_datetime_precision(Time.zone.parse(value).in_time_zone, max_precision)) } end @@ -241,13 +241,17 @@ context "when date" do let(:value) { random_date } - it { is_expected.to eq(value.to_time(:utc).in_time_zone) } + it { is_expected.to eq(apply_datetime_precision(value.to_time(:utc).in_time_zone, args[:precision])) } end context "when date string" do let(:value) { random_date.iso8601 } - it { is_expected.to eq(Date.parse(value).to_time(:utc).in_time_zone) } + it do + expect(call_method).to eq( + apply_datetime_precision(Date.parse(value).to_time(:utc).in_time_zone, args[:precision]), + ) + end end context "when datetime" do @@ -263,7 +267,7 @@ end context "when datetime string with fractional seconds digits" do - let(:value) { random_datetime.utc.iso8601(max_precision) } + let(:value) { random_datetime.utc.iso8601(max_precision + 1) } it { is_expected.to eq(apply_datetime_precision(Time.zone.parse(value).in_time_zone, args[:precision])) } end diff --git a/spec/lib/active_fields/casters/decimal_array_caster_spec.rb b/spec/lib/active_fields/casters/decimal_array_caster_spec.rb index 9b25d8b..e044a73 100644 --- a/spec/lib/active_fields/casters/decimal_array_caster_spec.rb +++ b/spec/lib/active_fields/casters/decimal_array_caster_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.describe ActiveFields::Casters::DecimalArrayCaster do + max_precision = ActiveFields::MAX_DECIMAL_PRECISION + let(:object) { described_class.new(**args) } let(:args) { {} } @@ -26,15 +28,15 @@ end context "when array of numbers" do - let(:value) { random_numbers } + let(:value) { [random_integer, random_float, random_decimal(max_precision + 1)] } - it { is_expected.to eq(value.map { _1.to_d.to_s }) } + it { is_expected.to eq(value.map { _1.to_d.truncate(max_precision).to_s }) } end context "when array of number strings" do - let(:value) { [random_integer.to_s, random_decimal.to_s] } + let(:value) { [random_integer.to_s, random_decimal(max_precision + 1).to_s] } - it { is_expected.to eq(value.map { _1.to_d.to_s }) } + it { is_expected.to eq(value.map { _1.to_d.truncate(max_precision).to_s }) } end context "when not an array" do @@ -44,16 +46,16 @@ end context "with precision" do - let(:args) { { precision: rand(0..10) } } + let(:args) { { precision: rand(0..max_precision) } } context "when array of numbers" do - let(:value) { random_numbers } + let(:value) { [random_integer, random_float, random_decimal(max_precision + 1)] } it { is_expected.to eq(value.map { _1.to_d.truncate(args[:precision]).to_s }) } end context "when array of number strings" do - let(:value) { [random_integer.to_s, random_decimal.to_s] } + let(:value) { [random_integer.to_s, random_decimal(max_precision + 1).to_s] } it { is_expected.to eq(value.map { _1.to_d.truncate(args[:precision]).to_s }) } end @@ -82,34 +84,34 @@ end context "when array of numbers" do - let(:value) { random_numbers } + let(:value) { [random_integer, random_float, random_decimal(max_precision + 1)] } - it { is_expected.to eq(value.map(&:to_d)) } + it { is_expected.to eq(value.map { _1.to_d.truncate(max_precision) }) } end context "when array of number strings" do - let(:value) { [random_integer.to_s, random_decimal.to_s] } + let(:value) { [random_integer.to_s, random_decimal(max_precision + 1).to_s] } - it { is_expected.to eq(value.map(&:to_d)) } + it { is_expected.to eq(value.map { _1.to_d.truncate(max_precision) }) } end context "when not an array" do - let(:value) { [random_integer.to_s, random_decimal.to_s, *random_numbers].sample } + let(:value) { [random_integer.to_s, random_decimal(max_precision + 1).to_s, *random_numbers].sample } it { is_expected.to be_nil } end context "with precision" do - let(:args) { { precision: rand(0..10) } } + let(:args) { { precision: rand(0..max_precision) } } context "when array of numbers" do - let(:value) { random_numbers } + let(:value) { [random_integer, random_float, random_decimal(max_precision + 1)] } it { is_expected.to eq(value.map { _1.to_d.truncate(args[:precision]) }) } end context "when array of number strings" do - let(:value) { [random_integer.to_s, random_decimal.to_s] } + let(:value) { [random_integer.to_s, random_decimal(max_precision + 1).to_s] } it { is_expected.to eq(value.map { _1.to_d.truncate(args[:precision]) }) } end diff --git a/spec/lib/active_fields/casters/decimal_caster_spec.rb b/spec/lib/active_fields/casters/decimal_caster_spec.rb index 6d08e40..455ae2a 100644 --- a/spec/lib/active_fields/casters/decimal_caster_spec.rb +++ b/spec/lib/active_fields/casters/decimal_caster_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.describe ActiveFields::Casters::DecimalCaster do + max_precision = ActiveFields::MAX_DECIMAL_PRECISION + let(:object) { described_class.new(**args) } let(:args) { {} } @@ -16,31 +18,31 @@ context "when integer" do let(:value) { random_integer } - it { is_expected.to eq(value.to_d.to_s) } + it { is_expected.to eq(value.to_d.truncate(max_precision).to_s) } end context "when float" do let(:value) { random_float } - it { is_expected.to eq(value.to_d.to_s) } + it { is_expected.to eq(value.to_d.truncate(max_precision).to_s) } end context "when big decimal" do - let(:value) { random_decimal } + let(:value) { random_decimal(max_precision + 1) } - it { is_expected.to eq(value.to_s) } + it { is_expected.to eq(value.truncate(max_precision).to_s) } end context "when integer string" do let(:value) { random_integer.to_s } - it { is_expected.to eq(value.to_d.to_s) } + it { is_expected.to eq(value.to_d.truncate(max_precision).to_s) } end context "when decimal string" do - let(:value) { random_decimal.to_s } + let(:value) { random_decimal(max_precision + 1).to_s } - it { is_expected.to eq(value.to_d.to_s) } + it { is_expected.to eq(value.to_d.truncate(max_precision).to_s) } end context "when invalid string" do @@ -56,7 +58,7 @@ end context "with precision" do - let(:args) { { precision: rand(0..10) } } + let(:args) { { precision: rand(0..max_precision) } } context "when integer" do let(:value) { random_integer } @@ -71,7 +73,7 @@ end context "when big decimal" do - let(:value) { random_decimal } + let(:value) { random_decimal(max_precision + 1) } it { is_expected.to eq(value.truncate(args[:precision]).to_s) } end @@ -83,7 +85,7 @@ end context "when decimal string" do - let(:value) { random_decimal.to_s } + let(:value) { random_decimal(max_precision + 1).to_s } it { is_expected.to eq(value.to_d.truncate(args[:precision]).to_s) } end @@ -102,31 +104,31 @@ context "when integer" do let(:value) { random_integer } - it { is_expected.to eq(value.to_d) } + it { is_expected.to eq(value.to_d.truncate(max_precision)) } end context "when float" do let(:value) { random_float } - it { is_expected.to eq(value.to_d) } + it { is_expected.to eq(value.to_d.truncate(max_precision)) } end context "when big decimal" do - let(:value) { random_decimal } + let(:value) { random_decimal(max_precision + 1) } - it { is_expected.to eq(value) } + it { is_expected.to eq(value.truncate(max_precision)) } end context "when integer string" do let(:value) { random_integer.to_s } - it { is_expected.to eq(value.to_d) } + it { is_expected.to eq(value.to_d.truncate(max_precision)) } end context "when decimal string" do - let(:value) { random_decimal.to_s } + let(:value) { random_decimal(max_precision + 1).to_s } - it { is_expected.to eq(value.to_d) } + it { is_expected.to eq(value.to_d.truncate(max_precision)) } end context "when invalid string" do @@ -142,7 +144,7 @@ end context "with precision" do - let(:args) { { precision: rand(0..10) } } + let(:args) { { precision: rand(0..max_precision) } } context "when integer" do let(:value) { random_integer } @@ -157,7 +159,7 @@ end context "when big decimal" do - let(:value) { random_decimal } + let(:value) { random_decimal(max_precision + 1) } it { is_expected.to eq(value.truncate(args[:precision])) } end @@ -169,7 +171,7 @@ end context "when decimal string" do - let(:value) { random_decimal.to_s } + let(:value) { random_decimal(max_precision + 1).to_s } it { is_expected.to eq(value.to_d.truncate(args[:precision])) } end diff --git a/spec/models/active_fields/field/date_time_array_spec.rb b/spec/models/active_fields/field/date_time_array_spec.rb index 6129e6b..a285fb4 100644 --- a/spec/models/active_fields/field/date_time_array_spec.rb +++ b/spec/models/active_fields/field/date_time_array_spec.rb @@ -122,7 +122,7 @@ end context "when precision is less than max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION - 1 } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION - 1 } it "is valid" do record.valid? @@ -132,7 +132,7 @@ end context "when precision is max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION } it "is valid" do record.valid? @@ -142,7 +142,7 @@ end context "when precision is greater than max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + 1 } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION + 1 } it "is invalid" do record.valid? @@ -150,7 +150,7 @@ errors = record.errors.where( :precision, :less_than_or_equal_to, - count: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, + count: ActiveFields::MAX_DATETIME_PRECISION, ) expect(errors).not_to be_empty end @@ -160,7 +160,7 @@ context "callbacks" do describe "before_save #reapply_precision" do - max_precision = ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + max_precision = ActiveFields::MAX_DATETIME_PRECISION let(:record) { build(factory) } let(:precision) { rand(0..(max_precision - 1)) } let(:attrs) do diff --git a/spec/models/active_fields/field/date_time_spec.rb b/spec/models/active_fields/field/date_time_spec.rb index d43a429..903cb4e 100644 --- a/spec/models/active_fields/field/date_time_spec.rb +++ b/spec/models/active_fields/field/date_time_spec.rb @@ -119,7 +119,7 @@ end context "when precision is less than max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION - 1 } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION - 1 } it "is valid" do record.valid? @@ -129,7 +129,7 @@ end context "when precision is max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION } it "is valid" do record.valid? @@ -139,7 +139,7 @@ end context "when precision is greater than max allowed" do - let(:precision) { ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + 1 } + let(:precision) { ActiveFields::MAX_DATETIME_PRECISION + 1 } it "is invalid" do record.valid? @@ -147,7 +147,7 @@ errors = record.errors.where( :precision, :less_than_or_equal_to, - count: ActiveFields::Casters::DateTimeCaster::MAX_PRECISION, + count: ActiveFields::MAX_DATETIME_PRECISION, ) expect(errors).not_to be_empty end @@ -176,7 +176,7 @@ end describe "before_save #reapply_precision" do - max_precision = ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + max_precision = ActiveFields::MAX_DATETIME_PRECISION let(:record) { build(factory) } let(:precision) { rand(0..(max_precision - 1)) } let(:attrs) do diff --git a/spec/models/active_fields/field/decimal_array_spec.rb b/spec/models/active_fields/field/decimal_array_spec.rb index f1da564..e37457d 100644 --- a/spec/models/active_fields/field/decimal_array_spec.rb +++ b/spec/models/active_fields/field/decimal_array_spec.rb @@ -121,8 +121,8 @@ end end - context "when precision is positive" do - let(:precision) { rand(1..10) } + context "when precision is less than max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION - 1 } it "is valid" do record.valid? @@ -130,12 +130,37 @@ expect(record.errors.where(:precision)).to be_empty end end + + context "when precision is max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION } + + it "is valid" do + record.valid? + + expect(record.errors.where(:precision)).to be_empty + end + end + + context "when precision is greater than max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION + 1 } + + it "is invalid" do + record.valid? + + errors = record.errors.where( + :precision, + :less_than_or_equal_to, + count: ActiveFields::MAX_DECIMAL_PRECISION, + ) + expect(errors).not_to be_empty + end + end end end context "callbacks" do describe "before_save #reapply_precision" do - max_precision = rand(16..32) # But may be more + max_precision = ActiveFields::MAX_DECIMAL_PRECISION let(:record) { build(factory) } let(:precision) { rand(0..(max_precision - 1)) } let(:attrs) do diff --git a/spec/models/active_fields/field/decimal_spec.rb b/spec/models/active_fields/field/decimal_spec.rb index 6e5d44f..5c4b648 100644 --- a/spec/models/active_fields/field/decimal_spec.rb +++ b/spec/models/active_fields/field/decimal_spec.rb @@ -118,8 +118,8 @@ end end - context "when precision is positive" do - let(:precision) { rand(1..10) } + context "when precision is less than max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION - 1 } it "is valid" do record.valid? @@ -127,6 +127,31 @@ expect(record.errors.where(:precision)).to be_empty end end + + context "when precision is max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION } + + it "is valid" do + record.valid? + + expect(record.errors.where(:precision)).to be_empty + end + end + + context "when precision is greater than max allowed" do + let(:precision) { ActiveFields::MAX_DECIMAL_PRECISION + 1 } + + it "is invalid" do + record.valid? + + errors = record.errors.where( + :precision, + :less_than_or_equal_to, + count: ActiveFields::MAX_DECIMAL_PRECISION, + ) + expect(errors).not_to be_empty + end + end end end @@ -151,7 +176,7 @@ end describe "before_save #reapply_precision" do - max_precision = rand(16..32) # But may be more + max_precision = ActiveFields::MAX_DECIMAL_PRECISION let(:record) { build(factory) } let(:precision) { rand(0..(max_precision - 1)) } let(:attrs) do diff --git a/spec/support/shared_examples/store_attribute_datetime.rb b/spec/support/shared_examples/store_attribute_datetime.rb index 4b0edc2..fa724c2 100644 --- a/spec/support/shared_examples/store_attribute_datetime.rb +++ b/spec/support/shared_examples/store_attribute_datetime.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples "store_attribute_datetime" do |attr_name, store_attr_name, klass| - max_precision = ActiveFields::Casters::DateTimeCaster::MAX_PRECISION + max_precision = ActiveFields::MAX_DATETIME_PRECISION describe "##{attr_name}" do subject(:call_method) { record.public_send(attr_name) } @@ -58,13 +58,13 @@ context "when internal value is a datetime string" do let(:internal_value) { random_datetime.utc.iso8601 } - it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } # precision is skipped + it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } end context "when internal value is a datetime string with fractional seconds digits" do - let(:internal_value) { random_datetime.utc.iso8601(max_precision) } + let(:internal_value) { random_datetime.utc.iso8601(max_precision + 1) } - it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } # precision is skipped + it { is_expected.to eq(apply_datetime_precision(Time.zone.parse(internal_value).in_time_zone, max_precision)) } end end @@ -96,13 +96,13 @@ context "when internal value is a datetime string" do let(:internal_value) { random_datetime.utc.iso8601 } - it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } # precision is skipped + it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } end context "when internal value is a datetime string with fractional seconds digits" do - let(:internal_value) { random_datetime.utc.iso8601(max_precision) } + let(:internal_value) { random_datetime.utc.iso8601(max_precision + 1) } - it { is_expected.to eq(Time.zone.parse(internal_value).in_time_zone) } # precision is skipped + it { is_expected.to eq(apply_datetime_precision(Time.zone.parse(internal_value).in_time_zone, max_precision)) } end end end @@ -159,8 +159,7 @@ it "sets datetime as string" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]) - .to eq(value.to_time(:utc).iso8601(max_precision)) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.to_time(:utc).iso8601(max_precision)) end end @@ -181,8 +180,7 @@ it "sets datetime as string" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]) - .to eq(value.utc.iso8601(max_precision)) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.utc.iso8601(max_precision)) end end @@ -198,7 +196,7 @@ end context "when value is a datetime string with fractional seconds digits" do - let(:value) { random_datetime.iso8601(max_precision) } + let(:value) { random_datetime.iso8601(max_precision + 1) } it "sets datetime as string" do call_method @@ -222,8 +220,7 @@ it "sets datetime as string" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]) - .to eq(value.to_time(:utc).iso8601(max_precision)) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.to_time(:utc).iso8601(max_precision)) end end @@ -244,8 +241,7 @@ it "sets datetime as string" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]) - .to eq(value.utc.iso8601(max_precision)) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.utc.iso8601(max_precision)) end end @@ -261,7 +257,7 @@ end context "when value is a datetime string with fractional seconds digits" do - let(:value) { random_datetime.iso8601(max_precision) } + let(:value) { random_datetime.iso8601(max_precision + 1) } it "sets datetime as string" do call_method diff --git a/spec/support/shared_examples/store_attribute_decimal.rb b/spec/support/shared_examples/store_attribute_decimal.rb index b17e3a5..38ccaec 100644 --- a/spec/support/shared_examples/store_attribute_decimal.rb +++ b/spec/support/shared_examples/store_attribute_decimal.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples "store_attribute_decimal" do |attr_name, store_attr_name, klass| + max_precision = ActiveFields::MAX_DECIMAL_PRECISION + describe "##{attr_name}" do subject(:call_method) { record.public_send(attr_name) } @@ -29,9 +31,9 @@ end context "when internal value is a big decimal" do - let(:internal_value) { random_decimal } + let(:internal_value) { random_decimal(max_precision + 1) } - it { is_expected.to eq(internal_value) } + it { is_expected.to eq(internal_value.truncate(max_precision)) } end context "when internal value is an integer string" do @@ -41,9 +43,9 @@ end context "when internal value is a decimal string" do - let(:internal_value) { random_decimal.to_s } + let(:internal_value) { random_decimal(max_precision + 1).to_s } - it { is_expected.to eq(internal_value.to_d) } + it { is_expected.to eq(internal_value.to_d.truncate(max_precision)) } end context "when internal value is invalid" do @@ -95,12 +97,12 @@ end context "when value is a big decimal" do - let(:value) { random_decimal } + let(:value) { random_decimal(max_precision + 1) } it "sets decimal" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.to_s) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.truncate(max_precision).to_s) end end @@ -115,12 +117,12 @@ end context "when value is a decimal string" do - let(:value) { random_decimal.to_s } + let(:value) { random_decimal(max_precision + 1).to_s } it "sets decimal" do call_method - expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.to_d.to_s) + expect(record.public_send(store_attr_name)[attr_name.to_s]).to eq(value.to_d.truncate(max_precision).to_s) end end diff --git a/spec/support/test_methods.rb b/spec/support/test_methods.rb index a354e72..67eb3b5 100644 --- a/spec/support/test_methods.rb +++ b/spec/support/test_methods.rb @@ -16,8 +16,8 @@ def random_float rand(-10.0..10.0) end - def random_decimal - rand(-10.0..10.0).to_d + def random_decimal(precision = 16) + BigDecimal("#{random_integer}.#{Array.new(precision - 1) { rand(0..9) }.join}#{rand(1..9)}", 0) end def random_numbers @@ -119,7 +119,7 @@ def active_value_for(active_field) when ActiveFields::Field::DateTime min = active_field.min || ((active_field.max || Time.current) - rand(0..10).days) max = active_field.max && active_field.max >= min ? active_field.max : min + rand(0..10).days - precision = [active_field.precision, ActiveFields::Casters::DateTimeCaster::MAX_PRECISION].compact.min + precision = [active_field.precision, ActiveFields::MAX_DATETIME_PRECISION].compact.min allowed = [apply_datetime_precision(rand(min..max), precision)] allowed << nil unless active_field.required? @@ -128,7 +128,7 @@ def active_value_for(active_field) when ActiveFields::Field::DateTimeArray min = active_field.min || ((active_field.max || Time.current) - rand(0..10).days) max = active_field.max && active_field.max >= min ? active_field.max : min + rand(0..10).days - precision = [active_field.precision, ActiveFields::Casters::DateTimeCaster::MAX_PRECISION].compact.min + precision = [active_field.precision, ActiveFields::MAX_DATETIME_PRECISION].compact.min min_size = [active_field.min_size, 0].compact.max max_size = @@ -142,14 +142,16 @@ def active_value_for(active_field) when ActiveFields::Field::Decimal min = active_field.min || ((active_field.max || 0) - rand(0.0..10.0)) max = active_field.max && active_field.max >= min ? active_field.max : min + rand(0.0..10.0) + precision = [active_field.precision, ActiveFields::MAX_DECIMAL_PRECISION].compact.min - allowed = [rand(min..max).then { active_field.precision ? _1.truncate(active_field.precision) : _1 }] + allowed = [rand(min..max).truncate(precision)] allowed << nil unless active_field.required? allowed.sample when ActiveFields::Field::DecimalArray min = active_field.min || ((active_field.max || 0) - rand(0.0..10.0)) max = active_field.max && active_field.max >= min ? active_field.max : min + rand(0.0..10.0) + precision = [active_field.precision, ActiveFields::MAX_DECIMAL_PRECISION].compact.min min_size = [active_field.min_size, 0].compact.max max_size = @@ -159,9 +161,7 @@ def active_value_for(active_field) min_size + rand(0..10) end - Array.new(rand(min_size..max_size)) do - rand(min..max).then { active_field.precision ? _1.truncate(active_field.precision) : _1 } - end + Array.new(rand(min_size..max_size)) { rand(min..max).truncate(precision) } when ActiveFields::Field::Enum allowed = active_field.allowed_values.dup || [] allowed << nil unless active_field.required?