11

I'm trying to figure out how to make it so that a subclass of OpenStruct (or any class for that matter), or hash, will raise a custom exception if I try to access an attribute that hasn't been set. I couldn't get define_method and method_missing to do this so I'm clueless how it should be done in Ruby.

Here's an example:

class Request < OpenStruct...

request = Request.new

begin
  request.non_existent_attr
rescue CustomError...

I could imagine it would have to be something like this:

class Hash
  # if trying to access key:
  # 1) key exists, return key
  # 2) key doesn't exist, raise exception
end

Edit: Attributes that exist shouldn't raise an exception. The functionality I'm looking for is so that I can just access attributes freely and if it happens not to exist my custom exception will be raised.

Seralize
  • 1,117
  • 11
  • 27

6 Answers6

9

OpenStruct defines singleton accessor methods on the object when you set a new member, so you can use respond_to? to see if the member is valid. In fact you can probably just capture any method not defined with method_missing and throw the error unless it's a setter method name, in which case you pass it along to super.

class WhinyOpenStruct < OpenStruct
  def method_missing(meth, *args)
    raise NoMemberError, "no #{meth} member set yet" unless meth.to_s.end_with?('=')
    super
  end
end
dbenhur
  • 20,008
  • 4
  • 48
  • 45
9

If you need a strict hash, simply:

class StrictHash < Hash
  alias [] fetch
end

It works as expected:

hash = StrictHash[foo: "bar"]

hash[:foo]
# => "bar"

hash[:qux]
# stricthash.rb:7:in `fetch': key not found: :qux (KeyError)
#         from stricthash.rb:7:in `<main>'
djanowski
  • 5,610
  • 1
  • 27
  • 17
  • 2
    What I was looking for was "dot access", not hash access. +1 For elegance, though. – Seralize Jun 19 '15 at 14:22
  • It can be handy to apply the alias to a single instance: `some_hash.instance_eval {alias [] fetch}`. This [can be applied recursively](https://stackoverflow.com/a/68108769/6243352) to convert an existing hash to raise on missing keys using brackets, but I wonder if there's a more elegant way. – ggorlen Jun 24 '21 at 02:02
6

I use something like

hash = { a: 2, b: 3 }

Struct.new(*hash.keys).new(*hash.values).freeze

to get an immutable object which will raise NoMethodError in case unexpected method is invoked

vbyno
  • 546
  • 1
  • 5
  • 5
1

In ruby, whenever you write object.horray the message horray is sent to the object object, that will return a value. Since every horray is a message. If the object don't respond to this message, you can't distinguish between the object don't having an attribute with this name or if it don't have a method with this name.

So unless you will assume that no method can have a typo, it is not possible to do what you want to.

fotanus
  • 19,618
  • 13
  • 77
  • 111
  • If you do this on an OpenStruct, you will no longer be able to make new members except at initialize. – dbenhur Jun 03 '13 at 21:04
1

I went with this solution which does exactly what I need:

class Request < Hash
  class RequestError < StandardError; end
  class MissingAttributeError < RequestError; end

  def initialize(hash)
    hash.each do |key, value|
      self[key] = value
    end
  end

  def [](key)
    unless self.include?(key)
      raise MissingAttributeError.new("Attribute '#{key}' not found in request")
    end

    super
  end
end
Seralize
  • 1,117
  • 11
  • 27
  • 2
    You can use the `fetch` method instead of overwritting `[]` here. http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch – Philippe Creux Sep 10 '13 at 08:07
0

It's brutal, but you could overwrite the new_ostruct_member method to generate an error:

require 'ostruct'

class CustomError < StandardError; end
os  = OpenStruct.new({:a=>1, :b=>1})
def os.new_ostruct_member(name) #just wrecking a single instance
  raise CustomError, "non-existing key #{name} called"
end

p os.a=3
p os.c=4 #=>non-existing key c called (CustomError)
steenslag
  • 79,051
  • 16
  • 138
  • 171