11

I am working on an API. For a better developer experience, I would like to report back to the user any easily-found issue with params. My code validates strings, integers, booleans, iso8601 dates, and domain specific list of values. I am looking into a way to validate if a string is a valid UUID. I am looking into possible options to do it.

sawa
  • 165,429
  • 45
  • 277
  • 381
bunufi
  • 654
  • 1
  • 6
  • 16
  • 1
    You could validate the format of the uuid with a regex. See https://stackoverflow.com/questions/7680771/rails-custom-validation-based-on-a-regex#7680958 – Samy Kacimi Nov 27 '17 at 10:35
  • PostgreSQL adapter has some validation for UUID you can check the implementation and can use in your model. https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb#L6 – Naren Sisodiya Nov 27 '17 at 10:40
  • thanks @NarenSisodiya, I should have mentioned in the question that the validation is not acting directly on the attribute of the class, therefore I am not sure how I could use it. In addition, our rails project is using Sequel. The hint to underlying regex is useful though. – bunufi Nov 27 '17 at 12:33

6 Answers6

15

Based on the prevalent suggestion to use regex:

def validate_uuid_format(uuid)
  uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
  return true if uuid_regex.match?(uuid.to_s.downcase)

  log_and_raise_error("Given argument is not a valid UUID: '#{format_argument_output(uuid)}'")
end

Please note that, this only checks if a string adheres to a 8-4-4-4-12 format and ignores any version checks.

bunufi
  • 654
  • 1
  • 6
  • 16
8

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"]}
Andreas Gebhard
  • 361
  • 2
  • 8
  • Do you know more about the mechanism used to fail gracefully with `nil` on invalid UUID? This is causing me some issue now and would like to understand more about which part of the code is responsible for this behavior. – David Revelo Dec 27 '22 at 08:36
  • 1
    @DavidRevelo no, at the moment I do not. However, the right starting point to dig into the Rails source code surely is the "valid?" method. You can find the definition of the method by asking an instance for it with `my_model_instance.method(:valid?).source_location`. Then navigate to the source code and follow through from there. – Andreas Gebhard Jan 11 '23 at 07:24
2

Use https://github.com/dpep/rspec-uuid :

gem 'rspec-uuid'

Then just test if it is uuid:

it { expect(user_uuid).to be_a_uuid }

Or, you can check for a specific UUID version:

it { expect(user_uuid).to be_a_uuid(version: 4) }
David Hempy
  • 5,373
  • 2
  • 40
  • 68
2

In case you need to verify parameter before passing it to Postgres - it is enough to check that string follows 8-4-4-4-12 hexadecimal format.

Short check for parameter:

uuid.to_s.match /\h{8}-(\h{4}-){3}\h{12}/

In human words:

  • 8 hexadecimal characters
  • 3 groups by 4 hexadecimal characters
  • 12 hexadecimal characters
Aleksandr K.
  • 1,338
  • 14
  • 21
1

You can use the gem uuid's UUID.validate(my_string) class method.

See https://www.rubydoc.info/gems/uuid/2.3.1/UUID#validate-class_method

Please note that it matches against several UUID formats.

Capripot
  • 1,354
  • 16
  • 26
Alexandre
  • 61
  • 4
-2

validate it using regular expression matcher, depends which version of UUID you are validating against. I am sure there are plenty resource out there for each UUID version's regular expression pattern.

Jin.X
  • 120
  • 7