Although my answer will slightly restrict the generality of the question, I hope that it is still interesting enough. This restriction is the assumption that you instantiate a new object based on the set of parameters that you want to check, start validation and then return the errors object unless nil.
# params[:lot] = { material_id: [SOME STRING], maybe: more_attributes }
lot = Lot.new params[:lot]
lot.valid?
This way you use Rails' built-in validation mechanisms. However, as of May 2020 there still does not seem to be native support for validating the format of an attribute as a UUID. With native, I mean something along the lines of:
# models/lot.rb
# material_id is of type string, as per db/schema.rb
validates :material_id,
uuid: true
Typing this in Rails 6.0.3 one gets:
ArgumentError (Unknown validator: 'UuidValidator')
The key to validating attributes as a UUID therefore is to generate a UuidValidator class and to make sure that Rails' internals find and use it naturally.
Inspired by the solution that Doug Puchalski of coderwall.com has suggested, in combination with the Rails API docs, I came up with this solution:
# lib/uuid_validator.rb
class UuidValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
msg = options[:message] || "is not a valid UUID"
record.errors.add(attribute, msg)
end
end
end
Now, assume that you instantiate a new Lot instance and erronously assign an integer as foreign key to material_id:
lot = Lot.new({material_id: 1})
lot.material_id
=> "1" # note the auto type cast based on schema definition
lot.valid?
=> false
lot.errors.messages
=> {:material_id=>["is not a valid UUID"]}
# now, assign a valid uuid to material_id
lot.material_id = SecureRandom.uuid
=> "8c0f2f01-8f8e-4e83-a2a0-f5dd2e63fc33"
lot.valid?
=> true
Important:
As soon as you change the data type of your attribute to uuid,
# db/schema.rb
create_table "lots", id: false, force: :cascade do |t|
#t.string "material_id"
t.uuid "material_id"
end
Rails 6 will automatically only accept valid uuids for assigns to material_id. When trying to assing anything but a vaild UUID string, it will instead fail graciously:
lot = Lot.new
# trying to assign an integer...
lot.material_id({material_id: 1})
# results in gracious failure
=> nil
# the same is true for 'nearly valid' UUID strings, note the four last chars
lot.material_id = "44ab2cc4-f9e5-45c9-a08d-de6a98c0xxxx"
=> nil
However, you will still get the correct validation response:
lot.valid?
=> false
lot.errors.messages
=> {:material_id=>["is not a valid UUID"]}