aetherknight / recursive-open-struct

OpenStruct subclass that returns nested hash attributes as RecursiveOpenStructs
Other
276 stars 54 forks source link

recursive_ostruct_class option #43

Closed thorstenhirsch closed 7 years ago

thorstenhirsch commented 8 years ago

Hi. In my project I subclassed RecursiveOpenStruct and added some checks to my custom initializer, e.g.:

raise "no :id" unless self.id

The error was thrown even when I provided a hash with an id:

Subclass.new({id: 1, deep: { a: 15, b: 20 }})

Problem was: my custom initializer was called recursively (for every nested level). I think that's a problem others might stumble upon, too. The behavior I expected was that my custom initializer is being called only once and nested levels are not being created recursively with Subclass.new, but with RecursiveOpenStruct.new. So I added an option that provides exactly that.

Default behavior not changed (you have to opt-in for RecursiveOpenStruct.new on nested levels). Test case added. Description added.

Anything missing?

aetherknight commented 8 years ago

I would recommend using the delegate pattern for your use-case rather than trying to subclass RecursiveOpenStruct and add yet another optional feature to RecursiveOpenStruct (the existing optional features create all sorts of problems). This would give you the behavior you are expecting and more control over custom behavior, although it isn't exactly analogous to building the feature into ROS itself:

require 'recursive-open-struct'
require 'delegate'

class SomeClass < DelegateClass(RecursiveOpenStruct)
  attr_accessor :ros

  def initialize(h)
    @ros = RecursiveOpenStruct.new(h)
    super(@ros)
  end
end

c = MyClass.new({a: 'a', b: { b: 'b', c: 'c'}})
# => #<RecursiveOpenStruct a="a", b={:b=>"b", :c=>"c"}>
# Actually a MyClass, but it is delegating #inspect to the ROS instance:
c.class
# => MyClass

# Existing getters and setters when the ROS is passed to super behave as expected:
c.a
# => "a"
c.a = 'b'
# => "b"
c.a
# => "b"
c.b.b = 'd'
# => "d"
c.b.b
# => "d"

# The one gotcha is that setters that do not exist on the delegate ROS object don't work on SomeClass:
c.foo = 'c'
# => NoMethodError: undefined method `foo=' for #<RecursiveOpenStruct a="b", b={:b=>"b", :c=>"c"}>
#    from .../lib/ruby/2.3.0/delegate.rb:87:in `method_missing'

# But you can set a new value on the ROS directly, and then it will show up on the MyClass instance:

c.ros.foo = 'asdf'
c.foo
# => "asdf"

I suspect the delegate implementation is asking the ROS object whether it has a method (the exception is raised from delegate.rb's implementation of method_missing), rather than trying to blindly call the method. This would be straightforward to implement by overriding method_missing.

aetherknight commented 7 years ago

Closing b/c there hasn't been any discussion or activity on this PR in a year. I am reluctant to add another optional feature to ROS that seems to me like there are better patterns for implementing the desired behavior.