Last active
July 3, 2024 01:39
-
-
Save rubiii/37d52acec8dd5d8edafac1bfb74c65c3 to your computer and use it in GitHub Desktop.
Rails validator for Postgres decimal type with custom precision and scale
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DecimalValidator < ActiveModel::EachValidator | |
def validate_each(record, attribute, value) | |
precision = options[:precision] | |
scale = options[:scale] | |
if !precision || !scale | |
raise ArgumentError, "#{self.class.name} expects :precision and :scale option" | |
end | |
original_value = record.public_send("#{attribute}_before_type_cast").to_s | |
real_wholes, real_decimals = original_value.split(".") | |
wholes = (precision - scale) + (scale - (real_decimals.try(:length) || 0)) | |
regex = /\A\d{1,#{wholes}}(\.\d{0,#{scale}})?\z/ | |
if regex !~ original_value | |
record.errors.add(attribute, :decimal_out_of_specs, precision: precision, scale: scale) | |
return | |
end | |
# Prevent PG::NumericValueOutOfRange: ERROR: numeric field overflow | |
# DETAIL: A field with precision 10, scale 4 must round to an absolute value less than 10^6. | |
if value.round >= 10 ** (precision - scale) | |
record.errors.add(attribute, :decimal_out_of_range, max: "10^#{precision - scale}") | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "test_helper" | |
class DecimalValidatorTest < ActiveSupport::TestCase | |
# unfortunately needs a real model to test Postgres errors | |
def valid_quantity?(quantity) | |
record = build(:test_model, quantity: quantity) | |
record.validate | |
errors = record.errors[:quantity] | |
errors.empty? && record.save | |
end | |
test "configuration matches testcases" do | |
validator = TestModel.validators_on(:quantity).find { |v| v.kind_of?(DecimalValidator) } | |
fail "Missing DecimalValidator" unless validator | |
configuration = validator.options | |
assert_equal 10, configuration[:precision] | |
assert_equal 4, configuration[:scale] | |
end | |
test "accepts valid values" do | |
assert valid_quantity?(0) | |
assert valid_quantity?("0.0001") | |
assert valid_quantity?("999999") | |
assert valid_quantity?("123456.1234") | |
assert valid_quantity?("999998.9999") | |
end | |
test "rejects invalid values" do | |
refute valid_quantity?(nil) | |
refute valid_quantity?("") | |
refute valid_quantity?(".") | |
refute valid_quantity?(".1") | |
refute valid_quantity?("string") | |
refute valid_quantity?(1_000_000) | |
refute valid_quantity?("0.12345") | |
refute valid_quantity?("1234567.1234") | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment