ruby-on-railsrails-roar

How to dynamically add properties to a ROAR representer?


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.


Solution

  • 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).