80

How do I turn a string into a class name, but only if that class already exists?

If Amber is already a class, I can get from a string to the class via:

Object.const_get("Amber")

or (in Rails)

"Amber".constantize

But either of these will fail with NameError: uninitialized constant Amber if Amber is not already a class.

My first thought is to use the defined? method, but it doesn't discriminate between classes that already exist and those that don't:

>> defined?("Object".constantize)
=> "method"
>> defined?("AClassNameThatCouldNotPossiblyExist".constantize)
=> "method"

So how do I test if a string names a class before I try to convert it? (Okay, how about a begin/rescue block to catch NameError errors? Too ugly? I agree...)

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • 3
    `defined?` in the example is exactly doing what it is supposed to do: It checks if the `constantize` method on a String object is defined. It doesn't care if the string contains "Object" or "AClassNameThatCouldNotPossiblyExist". – RobDil May 08 '17 at 15:34

6 Answers6

141

How about const_defined??

Remember in Rails, there is auto-loading in development mode, so it can be tricky when you are testing it out:

>> Object.const_defined?('Account')
=> false
>> Account
=> Account(id: integer, username: string, google_api_key: string, created_at: datetime, updated_at: datetime, is_active: boolean, randomize_search_results: boolean, contact_url: string, hide_featured_results: boolean, paginate_search_results: boolean)
>> Object.const_defined?('Account')
=> true
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Chris Cherry
  • 28,118
  • 6
  • 68
  • 71
24

In rails it's really easy:

amber = "Amber".constantize rescue nil
if amber # nil result in false
    # your code here
end
Andrew K
  • 1,339
  • 15
  • 26
Eiji
  • 340
  • 5
  • 14
  • 1
    The `rescue` was helpful because sometimes constants can be unloaded and checking with `const_defined?` will be false. – Spencer Jun 17 '16 at 18:52
  • 2
    Suppressing exceptions isn't recommended, read more here: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions – Andrew K Sep 11 '17 at 19:23
  • @AndrewK: in Ruby rescue is used really often - ofc I agree that it's not good; in Elixir world we try to not do this if there is no need for doing that, but I saw that lots of people use rescue in Ruby – Eiji Sep 12 '17 at 19:43
  • 1
    @Eiji, Agreed. I just wanted to mention it as people new to Ruby don't know that it's an anti-pattern and should be avoided. – Andrew K Sep 12 '17 at 20:56
  • If you have an error in the "Amber" class, this rescue will swallow it and you will have a heck of a time debugging it. – DGM Dec 17 '21 at 13:33
15

Inspired by @ctcherry's response above, here's a 'safe class method send', where class_name is a string. If class_name doesn't name a class, it returns nil.

def class_send(class_name, method, *args)
  Object.const_defined?(class_name) ? Object.const_get(class_name).send(method, *args) : nil
end

An even safer version which invokes method only if class_name responds to it:

def class_send(class_name, method, *args)
  return nil unless Object.const_defined?(class_name)
  c = Object.const_get(class_name)
  c.respond_to?(method) ? c.send(method, *args) : nil
end
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • 2
    p.s.: if you like this response, please up-vote ctcherry's response, since that's what pointed me in the right direction. – fearless_fool Apr 22 '11 at 18:54
6

It would appear that all the answers using the Object.const_defined? method are flawed. If the class in question has not been loaded yet, due to lazy loading, then the assertion will fail. The only way to achieve this definitively is like so:

  validate :adapter_exists

  def adapter_exists
    # cannot use const_defined because of lazy loading it seems
    Object.const_get("Irs::#{adapter_name}")
  rescue NameError => e
    errors.add(:adapter_name, 'does not have an IrsAdapter')
  end
TomDunning
  • 4,829
  • 1
  • 26
  • 33
2

I've created a validator to test if a string is a valid class name (or comma-separated list of valid class names):

class ClassValidator < ActiveModel::EachValidator
  def validate_each(record,attribute,value)
    unless value.split(',').map { |s| s.strip.constantize.is_a?(Class) rescue false }.all?
      record.errors.add attribute, 'must be a valid Ruby class name (comma-separated list allowed)'
    end
  end
end
Fred Willmore
  • 4,386
  • 1
  • 27
  • 36
1

Another approach, in case you want to get the class too. Will return nil if the class isn't defined, so you don't have to catch an exception.

class String
  def to_class(class_name)
    begin
      class_name = class_name.classify (optional bonus feature if using Rails)
      Object.const_get(class_name)
    rescue
      # swallow as we want to return nil
    end
  end
end

> 'Article'.to_class
class Article

> 'NoSuchThing'.to_class
nil

# use it to check if defined
> puts 'Hello yes this is class' if 'Article'.to_class
Hello yes this is class
mahemoff
  • 44,526
  • 36
  • 160
  • 222