Jonhnny Weslley
Jonhnny Weslley

Reputation: 1080

Custom Decimal Type for localized input using Rails 5's Attributes API

I am trying to create a custom Decimal Type using the Rails 5's Attributes API to accepting localized user input. It looks like below:

  class Decimal < ActiveRecord::Type::Decimal

    def cast(value)
      return unless value
      cast_value(value.is_a?(String) ? parse_from_string(value) : value)
    end

    def changed_in_place?(raw_old_value, new_value)
      raw_old_value != serialize(new_value)
    end

    def parse_from_string(value)
      delimiter = I18n.t('number.format.delimiter')
      separator = I18n.t('number.format.separator')
      value.gsub(delimiter, '_').gsub(separator, '.')
    end
  end

I also have a custom form builder to show a formatted value to the user. When submitting the form to create resources (models entities), it works fine. However, when submitting the form to update resources, the validates_numericality_of validator marks my custom attribute as invalid (not_a_number). After some research in active model's source code, I reached this piece of code in NumericalityValidator.

https://github.com/rails/rails/blob/6a1b7985602c5bfab4c8875ca9bf0d598e063a65/activemodel/lib/active_model/validations/numericality.rb#L26-L49

But I don't understand what I could change to make this works. Any ideas?!

Upvotes: 2

Views: 224

Answers (2)

Jonhnny Weslley
Jonhnny Weslley

Reputation: 1080

I made it work by changing my custom Decimal type.

      class Decimal < ActiveRecord::Type::Decimal

        def cast(value)
          return unless value

          if value.is_a?(String)
            if numeric_string?(value)
              value = value.to_s.to_numeric
            else
              return value
            end
          end
          cast_value(value)
        end

        def value_constructed_by_mass_assignment?(value)
          if value.is_a?(String)
            numeric_string?(value)
          else
            super
          end
        end

        def numeric_string?(value)
          number = value.to_s.gsub(/[.,]/, '.' => '', ',' => '.')
          /\A[-+]?\d+/.match?(number)
        end

      end

Upvotes: 1

arieljuod
arieljuod

Reputation: 15838

The validator uses a variable called raw_value. It tries to get that raw value from your object, check the lines 35 to 38.

I guess you can define a method on your model using your attribute's name with "_before_type_cast" to return a numeric value that the validator can use.

If your attribute is called, lets say, amount, you can do:

def amount_before_type_cast
  amount.to_number
end

Then you'll have to define a method on your custom type to turn it into a number, maybe something like:

def to_number
  value.gsub(/\D/,'').to_i #remove all non-digit and turn it into an integer
end

Upvotes: 1

Related Questions