0

I have a person object that gets a big hash of information about the person back from a webservice call (around 400 key value pairs). Each of the items in the hash is somewhat distinct, and has to be handled differently before displaying to the user (e.g. timestamp value converted, nils handled, or text otherwise transformed), etc, and the keys themselves are also important elsewhere and must be 'translated' from a fixed mapping list.

Currently the hash of attributes is accessed directly in a Rails view, and then the return value is cleaned up in a helper, as in

<%= clean_up(person.attributes["some_strangly_formatted_name"]) %>

or

<%= show_timestamp(person.attributes["some_nonhuman_time"]) %>

I'm trying to figure out how to assign each of these values to the person object itself dynamically, while also creating each of the attributes as its own object of a different class (e.g. Attribute) to move related behavior there, as the amount and complexity of helper methods is getting out of control.

Ideally in the view, I could call person.some_timestamp and it would return the human readable time or person.name and return a string of the person's name

I've tried assigning variables as shown here, but it seems like the attribute has to be pre-defined on the object, and I'm trying to avoid 400 attr_accessors and also trying to accommodate the possibility that new attributes could be added Set Attribute Dynamically of Ruby Object Having a hash, set object properties in Ruby DRY way to assign hash values to an object

the non-working method looks like

  def process_attributes(attributes)
    attributes.each {|k,v| public_send("#{k}=", v)}
  end

How can I accomplish this goal? More generally, does this strategy even make sense?

JJK
  • 7
  • 1
  • 5
  • 1
    Maybe you want to explore the [Model-View-Presenter](https://en.wikipedia.org/wiki/Model–view–presenter) design pattern. If the model handles presentation concerns things can get quite confused. Your presenter can be a proxy that by default just passes through, but can be customized to reformat things. – tadman Apr 05 '19 at 16:56
  • Could you use a JSON type for the DB column and then access the key/value pairs that way? – Beartech Apr 05 '19 at 21:12

1 Answers1

0

Edit

I started riffing on an answer below, but it got me thinking "there's usually a built-in way to do this in Ruby/Rails", I would check out SimpleDelgator and if anyone wants to take a crack at using that to create a model like PersonAttributor < SimpleDelgator and do your transformations inside that, I'd upvote it.


Not sure if this is the direction you're looking to go but you could do something with JSON serialization. If you happen to be using Postgres you could use the JASONB column type and store the data as JSON that can be queried directly. But plain serialization could happen like:

person.attributes = {'one' => 'something', 'two' => 'some_other_thing'}.to_json
=> "{\"one\":\"something\",\"two\":\"some_other_thing\"}"
person.save

JSON(person.attributes)
=> {"one"=>"something", "two"=>"some_other_thing"}

JSON(person.attributes)["one"]
=> "something"

This is a very simple, off the cuff example. It puts all of your key/value pairs into a column without creating a ton of attr_accessors. You could do the clean-up when the hash is first assigned to the person.

This doesn't cover your idea of moving those attributes into classes of their own. If you are looking to create these objects on the fly, I'm not sure what you're saving yourself by doing that. If you want them to be accessible as methods you could use OpenStruct in some way, here's one:

In your person model you could have a method:

class Person < ApplicationRecord
...
def attr_object
  OpenStruct.new(self.attributes)
end
...

You'd then get an object with all of your keys as methods and your values as the return of those methods...

os_obj = person.attr_object
=> #<OpenStruct one='something', two='something_else'....>

You now have an object that gives you the methods one and one=. You can apply all of your transformations to that object using the methods...

os_obj.one
=> 'something'
os_obj.one = 'something new'
=> 'something new'

You could create a model that inherits from Person and creates the OpenStruct object. It could also store the updated key/value pairs from the OpenStruct object by serializing it as JSON:

person.attr_store = os_obj.to_h.to_json
"{\"one\":\"something new\"....}"

new_obj = Openstruct.new(JSON(person.attr_store))
=> #<OpenStruct one='something_new', two='something_else'....>

Hope that leads to something useful for you.

Beartech
  • 6,173
  • 1
  • 18
  • 41
  • Given the constraints I have with how the data is being used in other parts of the app and the lack of control I have over the format of the JSON return from the webservice (no DB), I headed the direction of SimpleDelegator and I think it will serve me well, I really appreciate your comment. – JJK Apr 10 '19 at 13:56