hopsoft / universalid

Fast, recursive, optimized, URL-Safe serialization for any Ruby object
MIT License
364 stars 9 forks source link

ActiveModel::Model Support #40

Open pinzonjulian opened 4 months ago

pinzonjulian commented 4 months ago

Hey! I've managed to add ActiveModel::Model support like so:

# config/initializers/universal_id.rb

UniversalID::MessagePackFactory.register(
  type: ActiveModel::Model,
  packer: -> (model, packer) {
    unless model.respond_to?(:attributes)
      error = <<~STR
        #{model.class} must respond to #attributes. You can do this in two ways:
        Using ActiveModel::Serializers to define a custom `attributes` method
        https://guides.rubyonrails.org/active_model_basics.html#serialization

        Using ActiveModel::Attributes which provides `attributes` automatically
        https://api.rubyonrails.org/classes/ActiveModel/Attributes.html
      STR
      raise ArgumentError, error
    end
    packer.write(model.class)
    packer.write(model.attributes)
  },
  unpacker: -> (unpacker) {
    model_name = unpacker.read
    model_name.new(unpacker.read)
  }
)

With this, a class including ActiveModel that responds to attributes now works.

I wonder if this is the right way to do it though. What do you think @hopsoft ?

hopsoft commented 4 months ago

This looks like a clean solution. Will consider getting this into the library soon. Thanks.

pinzonjulian commented 2 months ago

I'm extending the feature I built with this and I believe there's an edge case when an Active Record class is a child of an Active Model object. This proposal doesn't cover and I'm not sure how to extend it to make it work.

class Journey
  include ActiveModel::Model
  attribute :itinerary # active record object goes here
end

# schema: [name]
class Itinerary < ApplicationRecord
end

journey = Journey.new
journey.itinerary = Itinerary.new(name: "Hello World")
journey.itinerary.name # => "Hello World"

payload = URI::UID.build(journey, { include_changes: true }).payload
decoded_journey = URI::UID.from_payload(payload).decode

decoded_journey.itinerary.name # => nil

I tried using include_changes, include_descendants and descendants_depth with no luck.

Any ideas?

pinzonjulian commented 2 months ago

I also tried registering the settings like so:

UniversalID::Settings.register :changed, { prepack: { include_blank: false}, database: { include_changes: true, include_descendants: true, descendant_depth: 2} }

This didn't work either.

I've traced it down to this line: UniversalID::Extensions::ActiveRecordBasePacker#prepack_database_options https://github.com/hopsoft/universalid/blob/4294d1171b67510c85dc05e0cfd02353adc58614/lib/universalid/extensions/active_record/base_packer.rb#L50

For some reason, it's not understanding that I want to include changes.