How to dynamically add properties to a ROAR representer?

2.3k views Asked by At

I am using ROAR to implement an API for a rails application. This application deals with tickets that can have attributes like a subject and a description, but also have user defined attributes. For simplicity lets assume a ticket looks like:

class Ticket
  attr_accessor :subject, :description

  def custom_attributes
    # in reality these attributes depend on the current ticket instance
    # they are not hard-coded into the class
    [['priority', 'high'], ['Operating System', 'Ubuntu']]
  end
end

The desired JSON output for such a ticket looks as follows:

{
  "subject": "Foo",
  "description": "Bar",
  "customField1": "high",
  "customField2": "Ubuntu"
}

Now you might already see the problem. All properties are immediate children of the root object, this means I can't write that up as representer:

class TicketRepresenter
  property :subject
  property :description

  # Need to iterate over instance members on the class level here...
end

Is there some mechanic that ROAR offers to accomplish that? E.g. a callback that is executed in the context of an actual instance, e.g.

def call_me_on_write
  represented.custom_attributes.each do |attribute|
    add_property('customField1', attribute[1])
  end
end

Is there something like this in ROAR that I have overlooked to accomplish this?

I looked in both the docs for ROAR and the docs for representable, but could not find anything.

Disclaimer

I tried to simplify the actual circumstances to make the question more readable. If you think that important information are missing, please tell me. I will thankfully provide more details.

Out of scope

Please do not discuss whether the chosen JSON format is a good/bad idea, I want to evaluate whether ROAR would support it.

2

There are 2 answers

0
NobodysNightmare On BEST ANSWER

I ended up dynamically creating classes from my basic representer:

class TicketRepresenter
  property :subject
  property :description

  def self.create(ticket, context = {})
    klass = Class.new(TicketRepresenter) # create a subclass of my representer
    ticket.custom_attributes.each do |attribute|
      # for each custom attribute in the actual instance insert a property into the created class
      property "customField#{attribute.id}".to_sym
               getter: -> (*) { attribute.value }
    end

    # return an instance of the class created above
    klass.new(ticket, context)
  end
end

Basically that means the actual representer class used to create the JSON is a different one for each Ticket.

If you wanted to read a Ticket back from JSON, it is neccessary to correctly initialize the representer so that the created representer class knows about your custom fields and also define setters.

You will now need to conventionally call the new create method instead of new. If you need your representer to be created by ROAR (e.g. for a collection), you can use the Polymorphic Object Creation mechanism of ROAR.

Note: The code above does not exactly fit the example of custom attributes posted in my question, but you should get the idea (in the example an attribute did not have members like id and value, but was list consisting of key and value).

0
Chico Carvalho On

I believe the best approach for the problem would be to use Roar's writer:. It completely turns control over the output to you by passing a handful of values it calls options to a provided lambda.

For example:

property :desired_property_name, writer: -> (represented:, doc:, **) do
  doc[:desired_key] = represented.desired_value
end

There are a lot of uses not covered by the github readme but which are documented on the Trailblazer website. This one in particular can be found at http://trailblazer.to/gems/representable/3.0/function-api.html#writer.

Cheers!