aetherknight / recursive-open-struct

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

Unable to deserialize recursively #71

Open wildmaples opened 1 year ago

wildmaples commented 1 year ago

Hi there! I believe this could be similar to this open issue: https://github.com/aetherknight/recursive-open-struct/issues/69 but I figure I'd make one for our use case.

It seems like we are unable to

irb(main):001:0> require 'recursive-open-struct'
=> true
irb(main):002:0> Marshal.load(Marshal.dump(RecursiveOpenStruct.new(red: [RecursiveOpenStruct.new]))).red
/Users/maple.ong/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:80:in `[]': undefined method `[]' for nil:NilClass (NoMethodError)

    elsif v.is_a?(Array) and @options[:recurse_over_arrays]
                                     ^^^^^^^^^^^^^^^^^^^^^^
    from /Users/maple.ong/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:142:in `block (2 levels) in new_ostruct_member'
    from (irb):2:in `<main>'
    from /Users/maple.ong/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.3/exe/irb:11:in `<top (required)>'
    from /Users/maple.ong/.rbenv/versions/3.1.2/bin/irb:25:in `load'
    from /Users/maple.ong/.rbenv/versions/3.1.2/bin/irb:25:in `<main>'

We are trying to upgrade our application to Ruby 3 but we are relying on this behaviour to work. Since it is broken on the latest version we cannot upgrade.

christhomson commented 1 year ago

We are seeing the same problem while attempting to upgrade one of our services to Ruby 3 as well. (Thanks for posting this - great timing!)

christhomson commented 1 year ago

It looks like marshal_load is passed the options like recurse_over_arrays but they are just passed in at the top level and it will fallback to using OpenStruct's marshal_load implementation which will assign them in as keys in the struct rather than interpreting them as options.

Something like this may help:

class RecursiveOpenStruct
  # …

  def marshal_load(attributes)
    @options ||= {}
    @sub_elements ||= {}

    self.class.default_options.keys.each do |option|
      @options[option] = attributes.delete(option) if attributes.key?(option)
    end

    @deep_dup = DeepDup.new(@options)

    super
  end
end

Note: I have never written one of these marshal_load methods before so I'm not confident that this follows all the best practices (it almost certainly doesn't :sweat_smile:). I'm not sure if it's common practice to re-do a lot of the same work that initialize typically does. This would also mean it wouldn't be possible to create a RecursiveOpenStruct that has any of the three option names as a key in the struct. Perhaps if we defined our own marshal_dump method then we could support all key names properly.