Normalize attribute before type cast

I am trying to normalize an attribute for both assignment and querying. The intended way is using normalizes, the problem here is that the normalizer runs after the value has been type cast, which is preventing sensible normalization in my case.

The use case is I must convert the binary string (of varying length) "\x26\xF8\xAF\x32" or an integer 653_831_986 into the string form "26:F8:AF:32" and store that string in database.

It may both be queried and assigned by either binary string or integer value and that is where normalizes falls apart.

class Model < ApplicationRecord
  normalizes :serial, with: lambda { |val|
    # likely, but not guaranteed it was type-cast from Integer
    if val.match?(/\A[1-9][0-9]*\z/)
      str = val.to_i.to_s(16).upcase
      str = "0#{str}" if str.length.odd?
      str.chars.each_slice(2).map(&:join).join(":")
    # already correctly formatted
    elsif val.match?(\A([A-F0-9]{2}:)*[A-F0-9]{2}\z)
      val
    # treat as binary string
    else
      val.bytes.map { it.to_s(16).upcase.rjust(2, "0") }.join(":")
    end
  }
end

It is not possible to check whether the input value was an integer, because it is cast to string before normalization is applied. This forces me to use a heuristic (which may fail) to determine if the original value was an integer.

When using a custom setter, this does not solve the problem of querying, only assignment. Is it possible to apply normalization before type casting? Or is it possible to use a custom setter and also normalize the value for querying?

You could create an active model, which runs before.

With „attribute :serial“ (no type casting) And your normalizer.

Then use that model to build your active record object.

Alternatively, create a custom serializer

It looks like instead of string type and a normalizer you should try a custom Type I don’t have much experience with this, but whenever you deal with assignment/query the #serialize(value) should be called (I presume it would be easier to distinguish a binary string at this stage, or if it’s an INT).

#deserialize should kick in each time a DB value is read, and you’d need to decide what the canonical representation is for the app logic.